@hraness/dawg 0.6.0 → 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 +126 -0
- package/DAWG.md +655 -141
- package/README.md +27 -25
- package/core/autotune.ts +1119 -0
- package/core/chords.ts +759 -23
- package/core/clips.ts +499 -0
- package/core/diff.ts +182 -99
- package/core/expression.ts +158 -6
- package/core/fx.ts +354 -32
- package/core/granular.ts +91 -0
- package/core/instruments.ts +180 -0
- package/core/keys.ts +573 -4
- package/core/loop.ts +5 -0
- package/core/lyrics.ts +297 -0
- package/core/master.ts +3 -3
- package/core/resonators.ts +306 -4
- package/core/routing.ts +165 -0
- package/core/score.ts +1181 -16
- package/core/sdk/eval-child.ts +7 -2
- package/core/sdk/eval.ts +35 -6
- package/core/sdk/print.ts +354 -6
- package/core/sdk/sync-lyrics.ts +49 -0
- package/core/sdk/v1.ts +3160 -69
- package/core/sections.ts +298 -24
- package/core/sing.ts +815 -0
- package/core/strings.ts +237 -2
- 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/core/winds.ts +652 -0
- package/guides/agent.md +29 -0
- package/guides/arrange.md +30 -0
- package/guides/audition.md +20 -14
- package/guides/automation.md +12 -6
- package/guides/chords.md +16 -14
- package/guides/effects.md +17 -15
- 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 -10
- package/guides/play.md +21 -13
- package/guides/project.md +25 -8
- package/guides/providers.md +20 -14
- package/guides/resample.md +32 -0
- 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 -12
- 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 +131 -13
- package/src/agent/calibration-tools.ts +53 -0
- package/src/agent/chord-tools.ts +153 -1
- 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 +91 -0
- package/src/agent/gateway.ts +246 -60
- package/src/agent/models.ts +53 -12
- package/src/agent/ops.ts +15 -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 +5 -1
- package/src/agent/provider.ts +22 -8
- package/src/agent/resample-tool.ts +131 -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 +307 -37
- package/src/agent/usage.ts +2 -2
- package/src/agent/voice-tools.ts +925 -0
- package/src/agent/xcb-agent.ts +16 -6
- package/src/argv.ts +38 -0
- package/src/audio/analysis.ts +253 -0
- package/src/audio/arrange.ts +58 -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 +171 -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/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +21 -4
- package/src/audio/effects/common.ts +57 -0
- package/src/audio/effects/convolution.ts +113 -5
- package/src/audio/effects/filter.ts +48 -69
- package/src/audio/effects/formant.ts +263 -0
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +206 -33
- package/src/audio/fit.ts +80 -3
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +67 -47
- package/src/audio/instruments.ts +36 -3
- package/src/audio/keys/calibration.ts +56 -0
- package/src/audio/keys/electric.ts +427 -0
- package/src/audio/keys/engine.ts +156 -20
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +72 -8
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/kits.ts +135 -6
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +247 -26
- package/src/audio/native.ts +615 -0
- package/src/audio/preview.ts +45 -3
- package/src/audio/render-worker.ts +2 -0
- package/src/audio/renderer.ts +2 -0
- package/src/audio/resample.ts +286 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +252 -24
- package/src/audio/samples.ts +46 -3
- package/src/audio/sing/analysis.ts +193 -0
- package/src/audio/sing/engine.ts +949 -0
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +699 -0
- package/src/audio/strings/engine.ts +274 -22
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- 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 +608 -85
- package/src/audio/winds/engine.ts +306 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +104 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/trims1.ts +297 -0
- package/src/audio/winds/voice.ts +492 -0
- 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 +104 -23
- package/src/commands/formant.ts +221 -0
- package/src/commands/fx.ts +105 -33
- package/src/commands/grammar.ts +558 -0
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +652 -339
- package/src/commands/history.ts +18 -0
- package/src/commands/keys.ts +293 -17
- package/src/commands/modal.ts +67 -4
- 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/resample.ts +281 -0
- package/src/commands/rhythm.ts +3 -0
- package/src/commands/rig.ts +8 -25
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/sing.ts +478 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +485 -0
- 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 +244 -0
- 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 +1521 -255
- package/src/media/cli.ts +20 -3
- package/src/media/import.ts +3 -1
- package/src/project/check.ts +26 -3
- 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 +36 -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 +50 -7
- 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 +1377 -193
- package/src/tui/modal-menu.ts +38 -11
- package/src/tui/performance-menu.ts +46 -1
- package/src/tui/play-chords.ts +108 -5
- package/src/tui/play-mode.ts +16 -1
- package/src/tui/play-session.ts +81 -16
- 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 +144 -0
- 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,8 +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
|
-
import { modalSettings } from "./resonators.ts";
|
|
18
|
+
import { modalSettings, windSettings } from "./resonators.ts";
|
|
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";
|
|
18
29
|
import {
|
|
19
30
|
SCORE_LIMITS,
|
|
20
31
|
withNoteCap,
|
|
@@ -22,6 +33,7 @@ import {
|
|
|
22
33
|
TrackScore,
|
|
23
34
|
isSamplerInstrument,
|
|
24
35
|
wavetableOf,
|
|
36
|
+
type AudioClip,
|
|
25
37
|
type AutomationPoint,
|
|
26
38
|
type FormEntry,
|
|
27
39
|
type Note,
|
|
@@ -31,10 +43,10 @@ import {
|
|
|
31
43
|
} from "./score.ts";
|
|
32
44
|
import {
|
|
33
45
|
barStartTick,
|
|
34
|
-
bpmAtTick,
|
|
35
46
|
hasTrackTime,
|
|
36
47
|
normalizeSongTime,
|
|
37
48
|
performedNotes,
|
|
49
|
+
timeMapFor,
|
|
38
50
|
type Fermata,
|
|
39
51
|
type SongTime,
|
|
40
52
|
type TempoEvent,
|
|
@@ -60,6 +72,11 @@ export function sliceSongTime(
|
|
|
60
72
|
};
|
|
61
73
|
const clampBpm = (bpm: number) =>
|
|
62
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;
|
|
63
80
|
const sorted = [...pieces].sort((a, b) => a.offset - b.offset);
|
|
64
81
|
const first = sorted[0];
|
|
65
82
|
const tempoBpm = clampBpm(
|
|
@@ -146,7 +163,10 @@ export function bakeTrackTime(
|
|
|
146
163
|
() =>
|
|
147
164
|
new TrackScore({
|
|
148
165
|
...score.toJSON(),
|
|
149
|
-
tracks: score.tracks.map((
|
|
166
|
+
tracks: score.tracks.map((track) => {
|
|
167
|
+
const { time: _time, ...rest } = bakeClipTime(track);
|
|
168
|
+
return rest;
|
|
169
|
+
}),
|
|
150
170
|
notes,
|
|
151
171
|
}),
|
|
152
172
|
);
|
|
@@ -393,8 +413,73 @@ export function applySectionChanges(score: TrackScore): TrackScore {
|
|
|
393
413
|
}
|
|
394
414
|
notes.push(cut);
|
|
395
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
|
+
});
|
|
396
445
|
if (!changed) return score;
|
|
397
|
-
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));
|
|
398
483
|
}
|
|
399
484
|
|
|
400
485
|
/**
|
|
@@ -432,7 +517,14 @@ export function sectionScore(score: TrackScore, section: Section): TrackScore {
|
|
|
432
517
|
time: timed.time ?? null,
|
|
433
518
|
bars: section.bars,
|
|
434
519
|
tracks: score.tracks.map((track) =>
|
|
435
|
-
|
|
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,
|
|
527
|
+
),
|
|
436
528
|
),
|
|
437
529
|
notes,
|
|
438
530
|
sections: [],
|
|
@@ -598,6 +690,77 @@ function checkBars(bars: number): void {
|
|
|
598
690
|
);
|
|
599
691
|
}
|
|
600
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
|
+
|
|
601
764
|
/** Insert `count` empty bars at `atBar`, shifting later music and sections. */
|
|
602
765
|
export function insertBars(
|
|
603
766
|
score: TrackScore,
|
|
@@ -609,16 +772,32 @@ export function insertBars(
|
|
|
609
772
|
const ticks = barTicks(score);
|
|
610
773
|
const at = atBar * ticks;
|
|
611
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
|
+
]);
|
|
612
779
|
return new TrackScore({
|
|
613
780
|
...score.toJSON(),
|
|
781
|
+
tempoBpm: ripple.tempoBpm,
|
|
782
|
+
time: ripple.time,
|
|
614
783
|
bars: score.bars + count,
|
|
615
784
|
tracks: score.tracks.map((track) =>
|
|
616
|
-
|
|
785
|
+
insertClipBars(
|
|
786
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
787
|
+
insertPoints(points, at, shift),
|
|
788
|
+
),
|
|
789
|
+
at,
|
|
790
|
+
shift,
|
|
791
|
+
),
|
|
617
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.
|
|
618
795
|
notes: score.notes.map((note) =>
|
|
619
796
|
note.startTick >= at
|
|
620
797
|
? { ...note, startTick: note.startTick + shift }
|
|
621
|
-
: note
|
|
798
|
+
: note.startTick + note.durationTicks > at
|
|
799
|
+
? { ...note, durationTicks: note.durationTicks + shift }
|
|
800
|
+
: note,
|
|
622
801
|
),
|
|
623
802
|
sections: score.sections.map((section) =>
|
|
624
803
|
section.startBar >= atBar
|
|
@@ -636,6 +815,12 @@ export function deleteBars(
|
|
|
636
815
|
atBar: number,
|
|
637
816
|
count: number,
|
|
638
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;
|
|
639
824
|
if (count <= 0) return score;
|
|
640
825
|
if (count >= score.bars)
|
|
641
826
|
throw new ScoreValidationError("a song keeps at least one bar");
|
|
@@ -643,16 +828,21 @@ export function deleteBars(
|
|
|
643
828
|
const at = atBar * ticks;
|
|
644
829
|
const end = (atBar + count) * ticks;
|
|
645
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;
|
|
646
836
|
const notes: Note[] = [];
|
|
647
837
|
for (const note of score.notes) {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
+
);
|
|
656
846
|
}
|
|
657
847
|
const sections: Section[] = [];
|
|
658
848
|
for (const section of score.sections) {
|
|
@@ -673,11 +863,23 @@ export function deleteBars(
|
|
|
673
863
|
}
|
|
674
864
|
}
|
|
675
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
|
+
]);
|
|
676
870
|
return new TrackScore({
|
|
677
871
|
...score.toJSON(),
|
|
872
|
+
tempoBpm: ripple.tempoBpm,
|
|
873
|
+
time: ripple.time,
|
|
678
874
|
bars: score.bars - count,
|
|
679
875
|
tracks: score.tracks.map((track) =>
|
|
680
|
-
|
|
876
|
+
deleteClipBars(
|
|
877
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
878
|
+
deletePoints(points, at, shift),
|
|
879
|
+
),
|
|
880
|
+
at,
|
|
881
|
+
end,
|
|
882
|
+
),
|
|
681
883
|
),
|
|
682
884
|
notes,
|
|
683
885
|
sections,
|
|
@@ -861,6 +1063,15 @@ export function duplicateSection(
|
|
|
861
1063
|
if (options.as !== undefined && findSection(score, options.as))
|
|
862
1064
|
throw new ScoreValidationError(`a section named ${options.as} exists`);
|
|
863
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
|
+
);
|
|
864
1075
|
const opened = insertBars(score, at, section.bars);
|
|
865
1076
|
const source =
|
|
866
1077
|
section.startBar >= at ? section.startBar + section.bars : section.startBar;
|
|
@@ -904,10 +1115,26 @@ export function copyBars(
|
|
|
904
1115
|
Math.min(note.durationTicks, from + length - note.startTick),
|
|
905
1116
|
),
|
|
906
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
|
+
]);
|
|
907
1125
|
return new TrackScore({
|
|
908
1126
|
...score.toJSON(),
|
|
1127
|
+
tempoBpm: ripple.tempoBpm,
|
|
1128
|
+
time: ripple.time,
|
|
909
1129
|
tracks: score.tracks.map((track) =>
|
|
910
|
-
|
|
1130
|
+
copyClipBars(
|
|
1131
|
+
mapAutomation(ripple.pedals(track), (points) =>
|
|
1132
|
+
copyPoints(points, from, length, to),
|
|
1133
|
+
),
|
|
1134
|
+
from,
|
|
1135
|
+
length,
|
|
1136
|
+
to,
|
|
1137
|
+
),
|
|
911
1138
|
),
|
|
912
1139
|
notes: [...kept, ...copies],
|
|
913
1140
|
});
|
|
@@ -1231,17 +1458,38 @@ export function arrangedSlice(
|
|
|
1231
1458
|
): TrackScore {
|
|
1232
1459
|
const ticks = barTicks(score);
|
|
1233
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 })[] = [];
|
|
1234
1465
|
const end = fromBar + bars;
|
|
1466
|
+
let previous: FormSegment | undefined;
|
|
1235
1467
|
for (const segment of formSegments(score)) {
|
|
1236
1468
|
const segEnd = segment.startBar + segment.bars;
|
|
1469
|
+
const before = previous;
|
|
1470
|
+
previous = segment;
|
|
1237
1471
|
if (segEnd <= fromBar || segment.startBar >= end) continue;
|
|
1238
1472
|
const start = Math.max(segment.startBar, fromBar);
|
|
1239
1473
|
const stop = Math.min(segEnd, end);
|
|
1240
1474
|
const from = (segment.section.startBar + start - segment.startBar) * ticks;
|
|
1241
|
-
|
|
1475
|
+
const piece = {
|
|
1242
1476
|
from,
|
|
1243
1477
|
to: from + (stop - start) * ticks,
|
|
1244
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
|
+
: {}),
|
|
1245
1493
|
});
|
|
1246
1494
|
}
|
|
1247
1495
|
const shift = fromBar * ticks;
|
|
@@ -1251,7 +1499,16 @@ export function arrangedSlice(
|
|
|
1251
1499
|
tempoBpm: timed.tempoBpm,
|
|
1252
1500
|
time: timed.time ?? null,
|
|
1253
1501
|
bars,
|
|
1254
|
-
tracks: score.tracks.map((track) =>
|
|
1502
|
+
tracks: score.tracks.map((track) =>
|
|
1503
|
+
sliceTrackClips(
|
|
1504
|
+
slicePedals(sliceLanes(track, pieces), pieces),
|
|
1505
|
+
clipPieces.map(({ section, ...piece }) => ({
|
|
1506
|
+
...piece,
|
|
1507
|
+
...clipPieceState(section, track.id),
|
|
1508
|
+
})),
|
|
1509
|
+
score,
|
|
1510
|
+
),
|
|
1511
|
+
),
|
|
1255
1512
|
notes: arrangedNotes(score, fromBar, fromBar + noteBars).map((note) => ({
|
|
1256
1513
|
...note,
|
|
1257
1514
|
startTick: note.startTick - shift,
|
|
@@ -1333,7 +1590,13 @@ function laneFallback(track: Track, lane: string): number | undefined {
|
|
|
1333
1590
|
? track.keys
|
|
1334
1591
|
: spec.effect === "modal"
|
|
1335
1592
|
? track.modal && modalSettings(track.modal)
|
|
1336
|
-
:
|
|
1593
|
+
: spec.effect === "wind"
|
|
1594
|
+
? track.wind && windSettings(track.wind)
|
|
1595
|
+
: spec.effect === "sing"
|
|
1596
|
+
? track.sing && resolveSing(track.sing)
|
|
1597
|
+
: (track.fx as Record<string, unknown> | undefined)?.[
|
|
1598
|
+
spec.effect
|
|
1599
|
+
]
|
|
1337
1600
|
) as Record<string, unknown> | undefined;
|
|
1338
1601
|
const stored = source?.[spec.param];
|
|
1339
1602
|
return typeof stored === "number" ? stored : spec.spec.default;
|
|
@@ -1356,6 +1619,8 @@ const TOM = DRUM_VOICES.find((voice) => voice.voice === "tom")!.pitch;
|
|
|
1356
1619
|
const TOMS = [50, 47, TOM] as const;
|
|
1357
1620
|
const KICK = DRUM_VOICES.find((voice) => voice.voice === "kick")!.pitch;
|
|
1358
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;
|
|
1359
1624
|
|
|
1360
1625
|
/** Generator tracks, reused by later builds and drops. */
|
|
1361
1626
|
export const GENERATOR_TRACKS = Object.freeze({
|
|
@@ -1947,7 +2212,10 @@ export type FillOptions = Readonly<{
|
|
|
1947
2212
|
/** Beats the fill lasts, default 1. */
|
|
1948
2213
|
beats?: number;
|
|
1949
2214
|
style?: FillStyle;
|
|
1950
|
-
/**
|
|
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
|
+
*/
|
|
1951
2219
|
crash?: boolean;
|
|
1952
2220
|
}>;
|
|
1953
2221
|
|
|
@@ -1992,6 +2260,12 @@ export function generateFill(
|
|
|
1992
2260
|
drums = GENERATOR_TRACKS.fills;
|
|
1993
2261
|
}
|
|
1994
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;
|
|
1995
2269
|
const clamped: string[] = [];
|
|
1996
2270
|
const beats = clampNote(
|
|
1997
2271
|
options.beats ?? 1,
|
|
@@ -2022,8 +2296,8 @@ export function generateFill(
|
|
|
2022
2296
|
: SNARE
|
|
2023
2297
|
: progress < 0.25
|
|
2024
2298
|
? SNARE
|
|
2025
|
-
: // High, mid, low tom down the fill (GM 50/47/45);
|
|
2026
|
-
//
|
|
2299
|
+
: // High, mid, low tom down the fill (GM 50/47/45); a
|
|
2300
|
+
// calibrated song pitches them, older songs fold them.
|
|
2027
2301
|
TOMS[Math.min(2, Math.floor(((progress - 0.25) / 0.75) * 3))]!;
|
|
2028
2302
|
fill.push({
|
|
2029
2303
|
id: nextId(),
|
|
@@ -2035,7 +2309,7 @@ export function generateFill(
|
|
|
2035
2309
|
});
|
|
2036
2310
|
}
|
|
2037
2311
|
if ((options.crash ?? true) && end < next.bars * ticks)
|
|
2038
|
-
for (const pitch of [
|
|
2312
|
+
for (const pitch of [crash, KICK])
|
|
2039
2313
|
fill.push({
|
|
2040
2314
|
id: nextId(),
|
|
2041
2315
|
trackId: drums,
|