@hraness/dawg 0.6.1 → 0.8.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 +118 -0
- package/DAWG.md +454 -140
- package/README.md +30 -26
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +271 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +184 -104
- package/core/expression.ts +15 -0
- package/core/fx.ts +99 -29
- package/core/ids.ts +52 -0
- package/core/instruments.ts +19 -0
- package/core/keys.ts +3 -3
- package/core/loop.ts +8 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/range.ts +581 -0
- package/core/resonators.ts +16 -2
- package/core/routing.ts +165 -0
- package/core/score.ts +955 -14
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +287 -5
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +2132 -46
- package/core/sections.ts +480 -35
- 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 +31 -0
- package/guides/audio.md +31 -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 +15 -7
- package/guides/music.md +23 -8
- package/guides/notes.md +15 -9
- package/guides/panes.md +32 -0
- package/guides/performance.md +15 -13
- package/guides/play.md +21 -13
- package/guides/project.md +26 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +15 -9
- package/guides/rhythm.md +18 -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/tape.md +32 -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 +6 -2
- package/src/agent/agent.ts +130 -18
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/clip-tools.ts +453 -0
- package/src/agent/command-agent.ts +378 -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 +27 -1
- package/src/agent/pack-tools.ts +1 -1
- package/src/agent/planner.ts +25 -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/range-tools.ts +216 -0
- 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 +110 -23
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +13 -10
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +49 -5
- package/src/audio/audio-command.ts +218 -0
- 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/devices.ts +264 -0
- package/src/audio/doctor.ts +157 -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 +360 -40
- 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 +82 -32
- 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 +560 -0
- package/src/commands/help.ts +774 -366
- package/src/commands/history.ts +139 -10
- package/src/commands/keys.ts +25 -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 +135 -0
- package/src/commands/progression.ts +170 -0
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +3 -24
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +474 -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 +617 -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/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +511 -0
- package/src/launch-args.ts +267 -0
- package/src/main.ts +2508 -308
- 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/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +199 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/meta.ts +14 -0
- package/src/session/origin.ts +174 -0
- package/src/session/port.ts +109 -14
- package/src/session/presence.ts +34 -4
- package/src/session/protocol.ts +365 -7
- package/src/session/rebase.ts +63 -12
- package/src/session/receipt.ts +265 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +126 -45
- package/src/tui/arrange-menu.ts +225 -23
- package/src/tui/audition.ts +1 -1
- package/src/tui/euclid.ts +113 -27
- package/src/tui/fader.ts +282 -42
- package/src/tui/granular-menu.ts +2 -4
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- 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 +987 -179
- 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 +243 -28
- package/src/tui/sing-menu.ts +278 -0
- package/src/tui/style-menu.ts +104 -0
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -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 +491 -84
- package/tui/clip-row.ts +132 -0
- package/tui/delight.ts +193 -0
- package/tui/drawer.ts +233 -27
- package/tui/frame-gate.ts +76 -0
- package/tui/grammar.ts +143 -65
- package/tui/guide.ts +50 -5
- package/tui/highway.ts +309 -30
- package/tui/hints.ts +197 -0
- package/tui/hits.ts +7 -1
- package/tui/input.ts +60 -9
- package/tui/keys.ts +1 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +80 -14
- package/tui/prompt.ts +1 -1
- package/tui/screen.ts +151 -18
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +46 -1
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,
|
|
@@ -447,9 +535,9 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
|
|
|
447
535
|
// ---------------------------------------------------------------------------
|
|
448
536
|
// Automation over time edits
|
|
449
537
|
|
|
450
|
-
type Points = readonly AutomationPoint[];
|
|
538
|
+
export type Points = readonly AutomationPoint[];
|
|
451
539
|
|
|
452
|
-
const TRACK_LANES = [
|
|
540
|
+
export const TRACK_LANES = [
|
|
453
541
|
"volumeAutomation",
|
|
454
542
|
"panAutomation",
|
|
455
543
|
"filterAutomation",
|
|
@@ -460,7 +548,10 @@ const TRACK_LANES = [
|
|
|
460
548
|
] as const;
|
|
461
549
|
|
|
462
550
|
/** Applies `edit` to every automation lane of `track`. */
|
|
463
|
-
function mapAutomation(
|
|
551
|
+
export function mapAutomation(
|
|
552
|
+
track: Track,
|
|
553
|
+
edit: (points: Points) => Points,
|
|
554
|
+
): Track {
|
|
464
555
|
const out: Record<string, unknown> = { ...track };
|
|
465
556
|
let changed = false;
|
|
466
557
|
for (const field of TRACK_LANES) {
|
|
@@ -490,7 +581,7 @@ function mapAutomation(track: Track, edit: (points: Points) => Points): Track {
|
|
|
490
581
|
}
|
|
491
582
|
|
|
492
583
|
/** The lane value at `tick` (undefined before the first point). */
|
|
493
|
-
function valueAt(points: Points, tick: number): number | undefined {
|
|
584
|
+
export function valueAt(points: Points, tick: number): number | undefined {
|
|
494
585
|
const first = points[0];
|
|
495
586
|
if (!first || tick < first.tick) return undefined;
|
|
496
587
|
for (let index = 1; index < points.length; index += 1) {
|
|
@@ -505,7 +596,7 @@ function valueAt(points: Points, tick: number): number | undefined {
|
|
|
505
596
|
return points[points.length - 1]!.value;
|
|
506
597
|
}
|
|
507
598
|
|
|
508
|
-
function sortPoints(points: AutomationPoint[]): Points {
|
|
599
|
+
export function sortPoints(points: AutomationPoint[]): Points {
|
|
509
600
|
const byTick = new Map<number, AutomationPoint>();
|
|
510
601
|
for (const point of points) byTick.set(point.tick, point);
|
|
511
602
|
return [...byTick.values()].sort((a, b) => a.tick - b.tick);
|
|
@@ -594,7 +685,7 @@ function copyPoints(
|
|
|
594
685
|
// ---------------------------------------------------------------------------
|
|
595
686
|
// Ripple edits
|
|
596
687
|
|
|
597
|
-
function checkBars(bars: number): void {
|
|
688
|
+
export function checkBars(bars: number): void {
|
|
598
689
|
if (bars > SCORE_LIMITS.maxBars)
|
|
599
690
|
throw new ScoreValidationError(
|
|
600
691
|
`the song would be ${bars} bars; the limit is ${SCORE_LIMITS.maxBars}`,
|
|
@@ -602,6 +693,77 @@ function checkBars(bars: number): void {
|
|
|
602
693
|
);
|
|
603
694
|
}
|
|
604
695
|
|
|
696
|
+
/**
|
|
697
|
+
* The song tempo map, fermatas and pedal lanes for a ripple edit that lays
|
|
698
|
+
* `pieces` of the score end to end: they move with the music they belong
|
|
699
|
+
* to, as notes and automation do.
|
|
700
|
+
*/
|
|
701
|
+
function rippleTime(
|
|
702
|
+
score: TrackScore,
|
|
703
|
+
pieces: readonly { from: number; to: number; offset: number }[],
|
|
704
|
+
): Readonly<{
|
|
705
|
+
tempoBpm: number;
|
|
706
|
+
time: SongTime | null;
|
|
707
|
+
pedals: (track: Track) => Track;
|
|
708
|
+
}> {
|
|
709
|
+
const kept = pieces.filter((piece) => piece.to > piece.from);
|
|
710
|
+
const timed = sliceSongTime(
|
|
711
|
+
score,
|
|
712
|
+
kept.map((piece) => ({
|
|
713
|
+
...piece,
|
|
714
|
+
to: Math.min(piece.to, SCORE_LIMITS.maxTick + 1),
|
|
715
|
+
})),
|
|
716
|
+
);
|
|
717
|
+
return {
|
|
718
|
+
tempoBpm: timed.tempoBpm,
|
|
719
|
+
time: timed.time ?? null,
|
|
720
|
+
pedals: (track) => {
|
|
721
|
+
if (!track.pedal && !track.softPedal && !track.sostenuto) return track;
|
|
722
|
+
const out: Record<string, unknown> = { ...track };
|
|
723
|
+
for (const field of ["pedal", "softPedal", "sostenuto"] as const) {
|
|
724
|
+
const events = track[field];
|
|
725
|
+
if (!events) continue;
|
|
726
|
+
const next = ripplePedal(events, kept);
|
|
727
|
+
if (next) out[field] = next;
|
|
728
|
+
else delete out[field];
|
|
729
|
+
}
|
|
730
|
+
return out as Track;
|
|
731
|
+
},
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* A pedal lane for `pieces` laid end to end with no lift at the seams (unlike
|
|
737
|
+
* a render slice): each piece starts in the state its source was in, and a
|
|
738
|
+
* gap left between pieces (inserted bars) holds the state before it.
|
|
739
|
+
*/
|
|
740
|
+
function ripplePedal(
|
|
741
|
+
events: readonly PedalEvent[],
|
|
742
|
+
pieces: readonly { from: number; to: number; offset: number }[],
|
|
743
|
+
): readonly PedalEvent[] | undefined {
|
|
744
|
+
const byTick = new Map<number, PedalState>();
|
|
745
|
+
for (const { from, to, offset } of pieces) {
|
|
746
|
+
let state: PedalState = "up";
|
|
747
|
+
for (const event of events) {
|
|
748
|
+
if (event.tick > from) break;
|
|
749
|
+
state = event.state;
|
|
750
|
+
}
|
|
751
|
+
byTick.set(offset, state);
|
|
752
|
+
for (const event of events)
|
|
753
|
+
if (event.tick > from && event.tick < to)
|
|
754
|
+
byTick.set(event.tick - from + offset, event.state);
|
|
755
|
+
}
|
|
756
|
+
const out: PedalEvent[] = [];
|
|
757
|
+
let last: PedalState = "up";
|
|
758
|
+
for (const tick of [...byTick.keys()].sort((a, b) => a - b)) {
|
|
759
|
+
const state = byTick.get(tick)!;
|
|
760
|
+
if (state === last) continue;
|
|
761
|
+
out.push({ tick, state });
|
|
762
|
+
last = state;
|
|
763
|
+
}
|
|
764
|
+
return out.length > 0 ? out : undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
605
767
|
/** Insert `count` empty bars at `atBar`, shifting later music and sections. */
|
|
606
768
|
export function insertBars(
|
|
607
769
|
score: TrackScore,
|
|
@@ -613,16 +775,32 @@ export function insertBars(
|
|
|
613
775
|
const ticks = barTicks(score);
|
|
614
776
|
const at = atBar * ticks;
|
|
615
777
|
const shift = count * ticks;
|
|
778
|
+
const ripple = rippleTime(score, [
|
|
779
|
+
{ from: 0, to: at, offset: 0 },
|
|
780
|
+
{ from: at, to: Infinity, offset: at + shift },
|
|
781
|
+
]);
|
|
616
782
|
return new TrackScore({
|
|
617
783
|
...score.toJSON(),
|
|
784
|
+
tempoBpm: ripple.tempoBpm,
|
|
785
|
+
time: ripple.time,
|
|
618
786
|
bars: score.bars + count,
|
|
619
787
|
tracks: score.tracks.map((track) =>
|
|
620
|
-
|
|
788
|
+
insertClipBars(
|
|
789
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
790
|
+
insertPoints(points, at, shift),
|
|
791
|
+
),
|
|
792
|
+
at,
|
|
793
|
+
shift,
|
|
794
|
+
),
|
|
621
795
|
),
|
|
796
|
+
// A note's start and end stay with the music they sit in, so one held
|
|
797
|
+
// across the insertion point sounds on through the new bars.
|
|
622
798
|
notes: score.notes.map((note) =>
|
|
623
799
|
note.startTick >= at
|
|
624
800
|
? { ...note, startTick: note.startTick + shift }
|
|
625
|
-
: note
|
|
801
|
+
: note.startTick + note.durationTicks > at
|
|
802
|
+
? { ...note, durationTicks: note.durationTicks + shift }
|
|
803
|
+
: note,
|
|
626
804
|
),
|
|
627
805
|
sections: score.sections.map((section) =>
|
|
628
806
|
section.startBar >= atBar
|
|
@@ -631,6 +809,14 @@ export function insertBars(
|
|
|
631
809
|
? { ...section, bars: section.bars + count }
|
|
632
810
|
: section,
|
|
633
811
|
),
|
|
812
|
+
// The loop range moves (or grows) like a section.
|
|
813
|
+
loop: score.loop
|
|
814
|
+
? score.loop.startBar >= atBar
|
|
815
|
+
? { ...score.loop, startBar: score.loop.startBar + count }
|
|
816
|
+
: score.loop.startBar + score.loop.bars > atBar
|
|
817
|
+
? { ...score.loop, bars: score.loop.bars + count }
|
|
818
|
+
: score.loop
|
|
819
|
+
: null,
|
|
634
820
|
});
|
|
635
821
|
}
|
|
636
822
|
|
|
@@ -640,6 +826,12 @@ export function deleteBars(
|
|
|
640
826
|
atBar: number,
|
|
641
827
|
count: number,
|
|
642
828
|
): TrackScore {
|
|
829
|
+
if (!Number.isInteger(atBar) || atBar < 0 || !Number.isInteger(count))
|
|
830
|
+
throw new ScoreValidationError(
|
|
831
|
+
`bars to delete must start at a whole bar inside the song`,
|
|
832
|
+
);
|
|
833
|
+
// Only bars inside the song can go (a section may sit past its end).
|
|
834
|
+
count = Math.min(atBar + count, score.bars) - atBar;
|
|
643
835
|
if (count <= 0) return score;
|
|
644
836
|
if (count >= score.bars)
|
|
645
837
|
throw new ScoreValidationError("a song keeps at least one bar");
|
|
@@ -647,16 +839,21 @@ export function deleteBars(
|
|
|
647
839
|
const at = atBar * ticks;
|
|
648
840
|
const end = (atBar + count) * ticks;
|
|
649
841
|
const shift = count * ticks;
|
|
842
|
+
// Each note's start and end stay with the music they sit in; the part
|
|
843
|
+
// inside the deleted bars goes, so a note running out of them keeps its
|
|
844
|
+
// tail and one running into them is cut where they start.
|
|
845
|
+
const place = (tick: number) =>
|
|
846
|
+
tick < at ? tick : tick < end ? at : tick - shift;
|
|
650
847
|
const notes: Note[] = [];
|
|
651
848
|
for (const note of score.notes) {
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
849
|
+
const startTick = place(note.startTick);
|
|
850
|
+
const stop = place(note.startTick + note.durationTicks);
|
|
851
|
+
if (stop <= startTick) continue;
|
|
852
|
+
notes.push(
|
|
853
|
+
startTick === note.startTick && stop - startTick === note.durationTicks
|
|
854
|
+
? note
|
|
855
|
+
: { ...note, startTick, durationTicks: stop - startTick },
|
|
856
|
+
);
|
|
660
857
|
}
|
|
661
858
|
const sections: Section[] = [];
|
|
662
859
|
for (const section of score.sections) {
|
|
@@ -677,18 +874,48 @@ export function deleteBars(
|
|
|
677
874
|
}
|
|
678
875
|
}
|
|
679
876
|
const names = new Set(sections.map((section) => foldName(section.name)));
|
|
877
|
+
const ripple = rippleTime(score, [
|
|
878
|
+
{ from: 0, to: at, offset: 0 },
|
|
879
|
+
{ from: end, to: Infinity, offset: at },
|
|
880
|
+
]);
|
|
680
881
|
return new TrackScore({
|
|
681
882
|
...score.toJSON(),
|
|
883
|
+
tempoBpm: ripple.tempoBpm,
|
|
884
|
+
time: ripple.time,
|
|
682
885
|
bars: score.bars - count,
|
|
683
886
|
tracks: score.tracks.map((track) =>
|
|
684
|
-
|
|
887
|
+
deleteClipBars(
|
|
888
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
889
|
+
deletePoints(points, at, shift),
|
|
890
|
+
),
|
|
891
|
+
at,
|
|
892
|
+
end,
|
|
893
|
+
),
|
|
685
894
|
),
|
|
686
895
|
notes,
|
|
687
896
|
sections,
|
|
688
897
|
form: score.form.filter((entry) => names.has(foldName(entry.section))),
|
|
898
|
+
loop: score.loop ? shrinkRange(score.loop, atBar, count) : null,
|
|
689
899
|
});
|
|
690
900
|
}
|
|
691
901
|
|
|
902
|
+
/** A bar range after bars `atBar..atBar+count` go (null when it goes too). */
|
|
903
|
+
function shrinkRange(
|
|
904
|
+
range: Readonly<{ startBar: number; bars: number }>,
|
|
905
|
+
atBar: number,
|
|
906
|
+
count: number,
|
|
907
|
+
): { startBar: number; bars: number } | null {
|
|
908
|
+
const until = range.startBar + range.bars;
|
|
909
|
+
if (until <= atBar) return range;
|
|
910
|
+
if (range.startBar >= atBar + count)
|
|
911
|
+
return { startBar: range.startBar - count, bars: range.bars };
|
|
912
|
+
const left = Math.max(0, atBar - range.startBar);
|
|
913
|
+
const right = Math.max(0, until - (atBar + count));
|
|
914
|
+
return left + right > 0
|
|
915
|
+
? { startBar: Math.min(range.startBar, atBar), bars: left + right }
|
|
916
|
+
: null;
|
|
917
|
+
}
|
|
918
|
+
|
|
692
919
|
/** Unused note ids `prefix1`, `prefix2`, ... */
|
|
693
920
|
function idMaker(score: TrackScore, prefix: string): () => string {
|
|
694
921
|
const used = new Set(score.notes.map((note) => note.id));
|
|
@@ -865,6 +1092,15 @@ export function duplicateSection(
|
|
|
865
1092
|
if (options.as !== undefined && findSection(score, options.as))
|
|
866
1093
|
throw new ScoreValidationError(`a section named ${options.as} exists`);
|
|
867
1094
|
const at = options.toBar ?? section.startBar + section.bars;
|
|
1095
|
+
if (
|
|
1096
|
+
!Number.isInteger(at) ||
|
|
1097
|
+
at < 0 ||
|
|
1098
|
+
at > score.bars ||
|
|
1099
|
+
(at > section.startBar && at < section.startBar + section.bars)
|
|
1100
|
+
)
|
|
1101
|
+
throw new ScoreValidationError(
|
|
1102
|
+
`a copy of ${section.name} goes at a bar 1..${score.bars + 1} outside it`,
|
|
1103
|
+
);
|
|
868
1104
|
const opened = insertBars(score, at, section.bars);
|
|
869
1105
|
const source =
|
|
870
1106
|
section.startBar >= at ? section.startBar + section.bars : section.startBar;
|
|
@@ -881,24 +1117,49 @@ export function duplicateSection(
|
|
|
881
1117
|
);
|
|
882
1118
|
}
|
|
883
1119
|
|
|
884
|
-
/**
|
|
1120
|
+
/** Options for `copyBars`: which tracks, and whether the copy overdubs. */
|
|
1121
|
+
export type CopyBarsOptions = Readonly<{
|
|
1122
|
+
/** Only these tracks (default all). A track subset leaves tempo alone. */
|
|
1123
|
+
trackIds?: ReadonlySet<string>;
|
|
1124
|
+
/** Overdub: keep the destination's notes (default replaces them). */
|
|
1125
|
+
merge?: boolean;
|
|
1126
|
+
}>;
|
|
1127
|
+
|
|
1128
|
+
/**
|
|
1129
|
+
* Copy the music of bars `from..from+count` onto bars `to..` (replacing it,
|
|
1130
|
+
* or with `merge` overdubbing it). All tracks bring the song's tempo and
|
|
1131
|
+
* fermatas with them; a track subset copies only its own notes, clips,
|
|
1132
|
+
* pedal and automation.
|
|
1133
|
+
*/
|
|
885
1134
|
export function copyBars(
|
|
886
1135
|
score: TrackScore,
|
|
887
1136
|
fromBar: number,
|
|
888
1137
|
count: number,
|
|
889
1138
|
toBar: number,
|
|
890
1139
|
idPrefix = "copy-",
|
|
1140
|
+
options: CopyBarsOptions = {},
|
|
891
1141
|
): TrackScore {
|
|
892
1142
|
const ticks = barTicks(score);
|
|
893
1143
|
const from = fromBar * ticks;
|
|
894
1144
|
const length = count * ticks;
|
|
895
1145
|
const to = toBar * ticks;
|
|
896
1146
|
const nextId = idMaker(score, idPrefix);
|
|
1147
|
+
const picked = (trackId: string): boolean =>
|
|
1148
|
+
options.trackIds === undefined || options.trackIds.has(trackId);
|
|
897
1149
|
const kept = score.notes.filter(
|
|
898
|
-
(note) =>
|
|
1150
|
+
(note) =>
|
|
1151
|
+
options.merge ||
|
|
1152
|
+
!picked(note.trackId) ||
|
|
1153
|
+
note.startTick < to ||
|
|
1154
|
+
note.startTick >= to + length,
|
|
899
1155
|
);
|
|
900
1156
|
const copies = score.notes
|
|
901
|
-
.filter(
|
|
1157
|
+
.filter(
|
|
1158
|
+
(note) =>
|
|
1159
|
+
picked(note.trackId) &&
|
|
1160
|
+
note.startTick >= from &&
|
|
1161
|
+
note.startTick < from + length,
|
|
1162
|
+
)
|
|
902
1163
|
.map((note) => ({
|
|
903
1164
|
...note,
|
|
904
1165
|
id: nextId(),
|
|
@@ -908,10 +1169,28 @@ export function copyBars(
|
|
|
908
1169
|
Math.min(note.durationTicks, from + length - note.startTick),
|
|
909
1170
|
),
|
|
910
1171
|
}));
|
|
1172
|
+
// The copied bars bring their tempo, fermatas and pedal; the music after
|
|
1173
|
+
// them goes on as it was.
|
|
1174
|
+
const ripple = rippleTime(score, [
|
|
1175
|
+
{ from: 0, to, offset: 0 },
|
|
1176
|
+
{ from, to: from + length, offset: to },
|
|
1177
|
+
{ from: to + length, to: Infinity, offset: to + length },
|
|
1178
|
+
]);
|
|
1179
|
+
const song = options.trackIds === undefined;
|
|
911
1180
|
return new TrackScore({
|
|
912
1181
|
...score.toJSON(),
|
|
1182
|
+
...(song ? { tempoBpm: ripple.tempoBpm, time: ripple.time } : {}),
|
|
913
1183
|
tracks: score.tracks.map((track) =>
|
|
914
|
-
|
|
1184
|
+
picked(track.id)
|
|
1185
|
+
? copyClipBars(
|
|
1186
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
1187
|
+
copyPoints(points, from, length, to),
|
|
1188
|
+
),
|
|
1189
|
+
from,
|
|
1190
|
+
length,
|
|
1191
|
+
to,
|
|
1192
|
+
)
|
|
1193
|
+
: track,
|
|
915
1194
|
),
|
|
916
1195
|
notes: [...kept, ...copies],
|
|
917
1196
|
});
|
|
@@ -1027,6 +1306,129 @@ export function resetSection(score: TrackScore, name: string): TrackScore {
|
|
|
1027
1306
|
});
|
|
1028
1307
|
}
|
|
1029
1308
|
|
|
1309
|
+
/**
|
|
1310
|
+
* Split a section at `atBar` (0-based, strictly inside it) into two: the
|
|
1311
|
+
* first keeps the name, the second is `<name> 2` (or `as`). Both keep the
|
|
1312
|
+
* mutes and variations; the form plays both halves wherever it played the
|
|
1313
|
+
* section, and a loop on it becomes the loop range over the same bars.
|
|
1314
|
+
*/
|
|
1315
|
+
export function splitSection(
|
|
1316
|
+
score: TrackScore,
|
|
1317
|
+
name: string,
|
|
1318
|
+
atBar: number,
|
|
1319
|
+
as?: string,
|
|
1320
|
+
): TrackScore {
|
|
1321
|
+
const section = requireSection(score, name);
|
|
1322
|
+
const end = section.startBar + section.bars;
|
|
1323
|
+
if (!Number.isInteger(atBar) || atBar <= section.startBar || atBar >= end)
|
|
1324
|
+
throw new ScoreValidationError(
|
|
1325
|
+
section.bars < 2
|
|
1326
|
+
? `${section.name} is one bar; nothing to split`
|
|
1327
|
+
: `split ${section.name} at a bar ${section.startBar + 2}..${end}`,
|
|
1328
|
+
);
|
|
1329
|
+
if (as !== undefined && findSection(score, as))
|
|
1330
|
+
throw new ScoreValidationError(`a section named ${as} exists`);
|
|
1331
|
+
const second = uniqueSectionName(score, as ?? `${section.name} 2`);
|
|
1332
|
+
const form: FormEntry[] = [];
|
|
1333
|
+
for (const entry of score.form) {
|
|
1334
|
+
if (foldName(entry.section) !== foldName(section.name)) {
|
|
1335
|
+
form.push(entry);
|
|
1336
|
+
continue;
|
|
1337
|
+
}
|
|
1338
|
+
for (let pass = 0; pass < (entry.repeat ?? 1); pass += 1)
|
|
1339
|
+
form.push({ section: section.name }, { section: second });
|
|
1340
|
+
}
|
|
1341
|
+
if (form.length > SCORE_LIMITS.maxFormEntries)
|
|
1342
|
+
throw new ScoreValidationError(
|
|
1343
|
+
`the form would have ${form.length} entries; the limit is ${SCORE_LIMITS.maxFormEntries}`,
|
|
1344
|
+
"score-limit",
|
|
1345
|
+
);
|
|
1346
|
+
const looping = score.loopSection === section.name;
|
|
1347
|
+
const split = score.withSections(
|
|
1348
|
+
score.sections.flatMap((candidate) =>
|
|
1349
|
+
candidate === section
|
|
1350
|
+
? [
|
|
1351
|
+
{ ...section, bars: atBar - section.startBar },
|
|
1352
|
+
{ ...section, name: second, startBar: atBar, bars: end - atBar },
|
|
1353
|
+
]
|
|
1354
|
+
: [candidate],
|
|
1355
|
+
),
|
|
1356
|
+
form,
|
|
1357
|
+
looping ? null : score.loopSection,
|
|
1358
|
+
);
|
|
1359
|
+
return looping
|
|
1360
|
+
? split.withLoop({ startBar: section.startBar, bars: section.bars })
|
|
1361
|
+
: split;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
/**
|
|
1365
|
+
* Join a section with the one that starts where it ends; it keeps the
|
|
1366
|
+
* first one's name, mutes and variations. Refused when the two mute or vary
|
|
1367
|
+
* differently (joining would change the sound) unless `force`, or when
|
|
1368
|
+
* the form plays the second anywhere but right after the first.
|
|
1369
|
+
*/
|
|
1370
|
+
export function joinSection(
|
|
1371
|
+
score: TrackScore,
|
|
1372
|
+
name: string,
|
|
1373
|
+
options: Readonly<{ force?: boolean }> = {},
|
|
1374
|
+
): TrackScore {
|
|
1375
|
+
const section = requireSection(score, name);
|
|
1376
|
+
const end = section.startBar + section.bars;
|
|
1377
|
+
const next = score.sections.find((candidate) => candidate.startBar === end);
|
|
1378
|
+
if (!next)
|
|
1379
|
+
throw new ScoreValidationError(
|
|
1380
|
+
`no section starts at bar ${end + 1}, right after ${section.name}`,
|
|
1381
|
+
);
|
|
1382
|
+
const sameMute =
|
|
1383
|
+
JSON.stringify([...(section.mute ?? [])].sort()) ===
|
|
1384
|
+
JSON.stringify([...(next.mute ?? [])].sort());
|
|
1385
|
+
if (
|
|
1386
|
+
!options.force &&
|
|
1387
|
+
(!sameMute ||
|
|
1388
|
+
JSON.stringify(section.vary ?? {}) !== JSON.stringify(next.vary ?? {}))
|
|
1389
|
+
)
|
|
1390
|
+
throw new ScoreValidationError(
|
|
1391
|
+
`${section.name} and ${next.name} mute or vary differently · section join ${section.name} force`,
|
|
1392
|
+
);
|
|
1393
|
+
const form: FormEntry[] = [];
|
|
1394
|
+
const entries = score.form;
|
|
1395
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1396
|
+
const entry = entries[index]!;
|
|
1397
|
+
const isFirst = foldName(entry.section) === foldName(section.name);
|
|
1398
|
+
const isSecond = foldName(entry.section) === foldName(next.name);
|
|
1399
|
+
const following = entries[index + 1];
|
|
1400
|
+
if (
|
|
1401
|
+
isFirst &&
|
|
1402
|
+
(entry.repeat ?? 1) === 1 &&
|
|
1403
|
+
following &&
|
|
1404
|
+
foldName(following.section) === foldName(next.name) &&
|
|
1405
|
+
(following.repeat ?? 1) === 1
|
|
1406
|
+
) {
|
|
1407
|
+
form.push({ section: section.name });
|
|
1408
|
+
index += 1;
|
|
1409
|
+
continue;
|
|
1410
|
+
}
|
|
1411
|
+
if (isFirst || isSecond)
|
|
1412
|
+
throw new ScoreValidationError(
|
|
1413
|
+
`the form plays ${section.name} and ${next.name} apart · change the form first`,
|
|
1414
|
+
);
|
|
1415
|
+
form.push(entry);
|
|
1416
|
+
}
|
|
1417
|
+
const looping =
|
|
1418
|
+
score.loopSection === section.name || score.loopSection === next.name;
|
|
1419
|
+
return score.withSections(
|
|
1420
|
+
score.sections
|
|
1421
|
+
.filter((candidate) => candidate !== next)
|
|
1422
|
+
.map((candidate) =>
|
|
1423
|
+
candidate === section
|
|
1424
|
+
? { ...section, bars: section.bars + next.bars }
|
|
1425
|
+
: candidate,
|
|
1426
|
+
),
|
|
1427
|
+
form,
|
|
1428
|
+
looping ? section.name : score.loopSection,
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1030
1432
|
function omit<T extends object, K extends keyof T>(
|
|
1031
1433
|
value: T,
|
|
1032
1434
|
key: K,
|
|
@@ -1235,17 +1637,38 @@ export function arrangedSlice(
|
|
|
1235
1637
|
): TrackScore {
|
|
1236
1638
|
const ticks = barTicks(score);
|
|
1237
1639
|
const pieces: { from: number; to: number; offset: number }[] = [];
|
|
1640
|
+
// Clips (0.7) are cut per section pass; a window's own edges inside a
|
|
1641
|
+
// pass (or where the form plays straight on) are seams, never cuts, so a
|
|
1642
|
+
// windowed render plays them as one pass does.
|
|
1643
|
+
const clipPieces: (ClipPiece & { section: Section })[] = [];
|
|
1238
1644
|
const end = fromBar + bars;
|
|
1645
|
+
let previous: FormSegment | undefined;
|
|
1239
1646
|
for (const segment of formSegments(score)) {
|
|
1240
1647
|
const segEnd = segment.startBar + segment.bars;
|
|
1648
|
+
const before = previous;
|
|
1649
|
+
previous = segment;
|
|
1241
1650
|
if (segEnd <= fromBar || segment.startBar >= end) continue;
|
|
1242
1651
|
const start = Math.max(segment.startBar, fromBar);
|
|
1243
1652
|
const stop = Math.min(segEnd, end);
|
|
1244
1653
|
const from = (segment.section.startBar + start - segment.startBar) * ticks;
|
|
1245
|
-
|
|
1654
|
+
const piece = {
|
|
1246
1655
|
from,
|
|
1247
1656
|
to: from + (stop - start) * ticks,
|
|
1248
1657
|
offset: (start - fromBar) * ticks,
|
|
1658
|
+
};
|
|
1659
|
+
pieces.push(piece);
|
|
1660
|
+
const straight =
|
|
1661
|
+
before !== undefined &&
|
|
1662
|
+
before.section.startBar + before.bars === segment.section.startBar;
|
|
1663
|
+
clipPieces.push({
|
|
1664
|
+
...piece,
|
|
1665
|
+
section: segment.section,
|
|
1666
|
+
...(stop < segEnd ? { to: from + (segEnd - start) * ticks } : {}),
|
|
1667
|
+
...(clipPieces.length === 0 &&
|
|
1668
|
+
fromBar > 0 &&
|
|
1669
|
+
(start > segment.startBar || straight)
|
|
1670
|
+
? { seam: true }
|
|
1671
|
+
: {}),
|
|
1249
1672
|
});
|
|
1250
1673
|
}
|
|
1251
1674
|
const shift = fromBar * ticks;
|
|
@@ -1256,7 +1679,14 @@ export function arrangedSlice(
|
|
|
1256
1679
|
time: timed.time ?? null,
|
|
1257
1680
|
bars,
|
|
1258
1681
|
tracks: score.tracks.map((track) =>
|
|
1259
|
-
|
|
1682
|
+
sliceTrackClips(
|
|
1683
|
+
slicePedals(sliceLanes(track, pieces), pieces),
|
|
1684
|
+
clipPieces.map(({ section, ...piece }) => ({
|
|
1685
|
+
...piece,
|
|
1686
|
+
...clipPieceState(section, track.id),
|
|
1687
|
+
})),
|
|
1688
|
+
score,
|
|
1689
|
+
),
|
|
1260
1690
|
),
|
|
1261
1691
|
notes: arrangedNotes(score, fromBar, fromBar + noteBars).map((note) => ({
|
|
1262
1692
|
...note,
|
|
@@ -1341,7 +1771,11 @@ function laneFallback(track: Track, lane: string): number | undefined {
|
|
|
1341
1771
|
? track.modal && modalSettings(track.modal)
|
|
1342
1772
|
: spec.effect === "wind"
|
|
1343
1773
|
? track.wind && windSettings(track.wind)
|
|
1344
|
-
:
|
|
1774
|
+
: spec.effect === "sing"
|
|
1775
|
+
? track.sing && resolveSing(track.sing)
|
|
1776
|
+
: (track.fx as Record<string, unknown> | undefined)?.[
|
|
1777
|
+
spec.effect
|
|
1778
|
+
]
|
|
1345
1779
|
) as Record<string, unknown> | undefined;
|
|
1346
1780
|
const stored = source?.[spec.param];
|
|
1347
1781
|
return typeof stored === "number" ? stored : spec.spec.default;
|
|
@@ -1364,6 +1798,8 @@ const TOM = DRUM_VOICES.find((voice) => voice.voice === "tom")!.pitch;
|
|
|
1364
1798
|
const TOMS = [50, 47, TOM] as const;
|
|
1365
1799
|
const KICK = DRUM_VOICES.find((voice) => voice.voice === "kick")!.pitch;
|
|
1366
1800
|
const OPEN_HAT = DRUM_VOICES.find((voice) => voice.voice === "openhat")!.pitch;
|
|
1801
|
+
/** GM crash 1, which calibration 1+ renders as a cymbal. */
|
|
1802
|
+
const CRASH = 49;
|
|
1367
1803
|
|
|
1368
1804
|
/** Generator tracks, reused by later builds and drops. */
|
|
1369
1805
|
export const GENERATOR_TRACKS = Object.freeze({
|
|
@@ -1955,7 +2391,10 @@ export type FillOptions = Readonly<{
|
|
|
1955
2391
|
/** Beats the fill lasts, default 1. */
|
|
1956
2392
|
beats?: number;
|
|
1957
2393
|
style?: FillStyle;
|
|
1958
|
-
/**
|
|
2394
|
+
/**
|
|
2395
|
+
* Crash with kick on the next downbeat, default on: GM 49 on a
|
|
2396
|
+
* calibrated song's synth kit, else an open hat.
|
|
2397
|
+
*/
|
|
1959
2398
|
crash?: boolean;
|
|
1960
2399
|
}>;
|
|
1961
2400
|
|
|
@@ -2000,6 +2439,12 @@ export function generateFill(
|
|
|
2000
2439
|
drums = GENERATOR_TRACKS.fills;
|
|
2001
2440
|
}
|
|
2002
2441
|
const style = options.style ?? "toms";
|
|
2442
|
+
// Sample kits and legacy songs have no cymbal voice: keep the open hat.
|
|
2443
|
+
const kit = next.tracks.find((track) => track.id === drums)?.kit;
|
|
2444
|
+
const crash =
|
|
2445
|
+
(next.calibration ?? 0) >= 1 && (kit === undefined || synthKit(kit))
|
|
2446
|
+
? CRASH
|
|
2447
|
+
: OPEN_HAT;
|
|
2003
2448
|
const clamped: string[] = [];
|
|
2004
2449
|
const beats = clampNote(
|
|
2005
2450
|
options.beats ?? 1,
|
|
@@ -2030,8 +2475,8 @@ export function generateFill(
|
|
|
2030
2475
|
: SNARE
|
|
2031
2476
|
: progress < 0.25
|
|
2032
2477
|
? SNARE
|
|
2033
|
-
: // High, mid, low tom down the fill (GM 50/47/45);
|
|
2034
|
-
//
|
|
2478
|
+
: // High, mid, low tom down the fill (GM 50/47/45); a
|
|
2479
|
+
// calibrated song pitches them, older songs fold them.
|
|
2035
2480
|
TOMS[Math.min(2, Math.floor(((progress - 0.25) / 0.75) * 3))]!;
|
|
2036
2481
|
fill.push({
|
|
2037
2482
|
id: nextId(),
|
|
@@ -2043,7 +2488,7 @@ export function generateFill(
|
|
|
2043
2488
|
});
|
|
2044
2489
|
}
|
|
2045
2490
|
if ((options.crash ?? true) && end < next.bars * ticks)
|
|
2046
|
-
for (const pitch of [
|
|
2491
|
+
for (const pitch of [crash, KICK])
|
|
2047
2492
|
fill.push({
|
|
2048
2493
|
id: nextId(),
|
|
2049
2494
|
trackId: drums,
|