@hraness/dawg 0.7.0 → 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 +26 -0
- package/DAWG.md +117 -30
- package/README.md +7 -5
- package/core/diff.ts +2 -0
- package/core/ids.ts +52 -0
- package/core/loop.ts +3 -0
- package/core/range.ts +581 -0
- package/core/score.ts +123 -1
- package/core/sdk/print.ts +11 -2
- package/core/sdk/v1.ts +281 -2
- package/core/sections.ts +198 -19
- package/guides/arrange.md +9 -8
- package/guides/audio.md +31 -0
- package/guides/mix.md +3 -1
- package/guides/panes.md +32 -0
- package/guides/project.md +3 -2
- package/guides/rhythm.md +2 -1
- package/guides/tape.md +32 -0
- package/package.json +2 -1
- package/src/agent/agent.ts +4 -4
- package/src/agent/command-agent.ts +19 -10
- package/src/agent/ops.ts +15 -0
- package/src/agent/planner.ts +12 -0
- package/src/agent/range-tools.ts +216 -0
- package/src/agent/show-me.ts +1 -1
- package/src/agent/tools.ts +2 -0
- package/src/agent/xcb-agent.ts +2 -3
- package/src/audio/arrange.ts +12 -2
- package/src/audio/audio-command.ts +218 -0
- package/src/audio/devices.ts +264 -0
- package/src/audio/doctor.ts +84 -13
- package/src/audio/engine.ts +216 -28
- package/src/commands/arrange.ts +60 -25
- package/src/commands/grammar.ts +3 -1
- package/src/commands/help.ts +187 -9
- package/src/commands/history.ts +122 -11
- package/src/commands/keys.ts +17 -0
- package/src/commands/parses.ts +6 -1
- package/src/commands/range.ts +763 -0
- package/src/commands/resample.ts +13 -10
- package/src/commands/sample.ts +11 -1
- package/src/commands/sing.ts +2 -6
- package/src/commands/vocal-pitch.ts +2 -1
- package/src/identity/actor.ts +127 -0
- package/src/lang/glossary.ts +18 -0
- package/src/launch-args.ts +105 -1
- package/src/main.ts +1142 -86
- package/src/session/client.ts +107 -3
- package/src/session/clipboard.ts +79 -0
- package/src/session/daemon.ts +196 -5
- package/src/session/live-host.ts +232 -0
- package/src/session/origin.ts +20 -0
- package/src/session/port.ts +87 -10
- package/src/session/protocol.ts +360 -6
- package/src/session/rebase.ts +46 -8
- package/src/session/receipt.ts +7 -0
- package/src/session/shared-live.ts +131 -0
- package/src/session/store.ts +61 -2
- package/src/tui/arrange-menu.ts +169 -1
- package/src/tui/euclid.ts +95 -14
- package/src/tui/fader.ts +55 -2
- package/src/tui/knob-fields.ts +107 -0
- package/src/tui/knob-map.ts +198 -0
- package/src/tui/menu.ts +228 -2
- package/src/tui/play-session.ts +196 -21
- package/src/tui/tape-mode.ts +580 -0
- package/src/tui/tape-view.ts +263 -0
- package/tui/app.ts +217 -44
- package/tui/delight.ts +49 -0
- package/tui/drawer.ts +167 -9
- package/tui/grammar.ts +34 -5
- package/tui/guide.ts +8 -1
- package/tui/highway.ts +40 -5
- package/tui/hints.ts +5 -0
- package/tui/hits.ts +7 -1
- package/tui/knobs.ts +268 -0
- package/tui/play-strip.ts +12 -0
- package/tui/screen.ts +7 -4
- package/tui/tape.ts +439 -0
- package/tui/text.ts +35 -2
- package/tui/theme.ts +19 -1
package/src/commands/help.ts
CHANGED
|
@@ -382,6 +382,14 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
382
382
|
entries: [
|
|
383
383
|
{ command: "volume <0..1>", summary: "track level" },
|
|
384
384
|
{ command: "pan <-1..1>", summary: "left … right" },
|
|
385
|
+
{
|
|
386
|
+
command: "knobs [sound|mix|master|tempo|fx <effect>]",
|
|
387
|
+
summary: "four knobs ● moves ▲ sizes ■ shapes ◆ level · tab all params",
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
command: "mix | volume <track> <0..1> | pan <track> <-1..1>",
|
|
391
|
+
summary: "the mixer page · volume drums 0.5 sets drums, focus stays",
|
|
392
|
+
},
|
|
385
393
|
{ command: "mute", summary: "silence this track" },
|
|
386
394
|
{ command: "unmute", summary: "hear it again" },
|
|
387
395
|
{ command: "solo", summary: "only this track" },
|
|
@@ -463,8 +471,61 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
463
471
|
command: "style list|search|info · style blend <a> <b> [w]",
|
|
464
472
|
summary: "the style tree · style search maqam",
|
|
465
473
|
},
|
|
466
|
-
{ command: "bars <count>", summary: "
|
|
467
|
-
{ command: "extend <count> bars", summary: "lengthen the
|
|
474
|
+
{ command: "bars <count>", summary: "song length, 1–256" },
|
|
475
|
+
{ command: "extend <count> bars", summary: "lengthen the song" },
|
|
476
|
+
],
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
group: "arrange · range",
|
|
480
|
+
entries: [
|
|
481
|
+
{
|
|
482
|
+
command: "/tape [on|off]",
|
|
483
|
+
summary: "every track across the bars · c copy x cut v paste · ctrl-t",
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
command: "keys record [replace|off]",
|
|
487
|
+
summary: "record the loop in play · one undo step a pass · r R on tape",
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
command: "loop <a>-<b> | <section> | next | prev | off",
|
|
491
|
+
summary: "the loop range · loop 5-6 · loop next steps it along",
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
command: "copy [<track>|all] <a>-<b> to <bar> [x<N>] [insert|merge]",
|
|
495
|
+
summary: "copy bass 5-6 to 7 x2 · copy all chorus to 17 insert",
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
command: "move [<track>|all] <a>-<b> to <bar> [insert]",
|
|
499
|
+
summary: "move bass 5-6 to 9 · the source empties",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
command: "clear [<track>|all] <a>-<b>",
|
|
503
|
+
summary: "clear bass 5-6 · empty bars, the bars stay",
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
command: "copy [<track>|all] <a>-<b> · paste [at <bar>] [x<N>]",
|
|
507
|
+
summary: "the clipboard · copy all 1-4 · paste at 9",
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
command: "reverse [<track>|all] <a>-<b>",
|
|
511
|
+
summary: "mirror the bars · reverse bass 5-6",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
command: "bars insert <n> at <bar> | bars remove <a>-<b>",
|
|
515
|
+
summary: "shift later music · sections, clips, automation, tempo",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
command: "jump <bar>[.<beat>] | <section>",
|
|
519
|
+
summary: "move the playhead · jump 5 · jump 5.3 · jump chorus",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
command: "section split <name> at <bar> | join <name>",
|
|
523
|
+
summary: "section split chorus at 13 · section join chorus",
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
command: "form print",
|
|
527
|
+
summary: "print form to tape · alias of form bake",
|
|
528
|
+
},
|
|
468
529
|
],
|
|
469
530
|
},
|
|
470
531
|
{
|
|
@@ -508,8 +569,14 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
508
569
|
command: "calibration [0|1|latest|off]",
|
|
509
570
|
summary: "sound fixes · 1 chokes hats, levels keys · 0 legacy",
|
|
510
571
|
},
|
|
511
|
-
{
|
|
512
|
-
|
|
572
|
+
{
|
|
573
|
+
command: "undo [all]",
|
|
574
|
+
summary: "this pane's last edit · Ctrl-Z · all: anyone's",
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
command: "redo [all]",
|
|
578
|
+
summary: "step forward · Ctrl-Y · all: anyone's",
|
|
579
|
+
},
|
|
513
580
|
{ command: "/status", summary: "name · revision · digest · storage" },
|
|
514
581
|
],
|
|
515
582
|
},
|
|
@@ -540,6 +607,39 @@ export const HELP_SECTIONS: readonly HelpSection[] = [
|
|
|
540
607
|
{ command: "/fork [<name>]", summary: "copy into a new session" },
|
|
541
608
|
],
|
|
542
609
|
},
|
|
610
|
+
{
|
|
611
|
+
group: "project · audio",
|
|
612
|
+
entries: [
|
|
613
|
+
{
|
|
614
|
+
command: "audio",
|
|
615
|
+
summary: "the output and input in use, and the lists",
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
command: "audio out <name|default>",
|
|
619
|
+
summary: "play through one output · saved on this machine",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
command: "audio in <name|default>",
|
|
623
|
+
summary: "the input to meter (recording takes comes later)",
|
|
624
|
+
},
|
|
625
|
+
{ command: "audio test", summary: "a short tone, then the input level" },
|
|
626
|
+
],
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
group: "project · panes",
|
|
630
|
+
entries: [
|
|
631
|
+
{ command: "panes", summary: "who has this session open, on what" },
|
|
632
|
+
{
|
|
633
|
+
command: "pane <screen> [<track>]",
|
|
634
|
+
summary: "copies dawg pane … for another terminal",
|
|
635
|
+
},
|
|
636
|
+
{ command: "pin · unpin", summary: "keep this pane on its track" },
|
|
637
|
+
{
|
|
638
|
+
command: "follow [<letter>] · unfollow",
|
|
639
|
+
summary: "follow another pane's track · follow b",
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
},
|
|
543
643
|
{
|
|
544
644
|
group: "agent",
|
|
545
645
|
entries: [
|
|
@@ -599,6 +699,65 @@ export const HELP_GUIDE: readonly HelpSection[] = [
|
|
|
599
699
|
},
|
|
600
700
|
];
|
|
601
701
|
|
|
702
|
+
/**
|
|
703
|
+
* `/help arrange`: one screen around the range verbs (design F11), each row
|
|
704
|
+
* a line to type. The full syntax of every arranging command stays in
|
|
705
|
+
* `/help all` and `/help <command>` (`/help copy`, `/help section`).
|
|
706
|
+
*/
|
|
707
|
+
export const ARRANGE_PAGE: HelpSection = {
|
|
708
|
+
group: "arrange",
|
|
709
|
+
entries: [
|
|
710
|
+
{
|
|
711
|
+
command: "loop 5-6 · loop chorus",
|
|
712
|
+
summary: "loop the bars · loop next · loop off",
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
command: "copy bass 5-6 to 7",
|
|
716
|
+
summary: "copy bars · x2 repeats · all = every track",
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
command: "move bass 5-6 to 9",
|
|
720
|
+
summary: "move bars · the source empties",
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
command: "clear bass 5-6",
|
|
724
|
+
summary: "empty bars · reverse bass 5-6 mirrors",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
command: "copy all 1-4 · paste at 9",
|
|
728
|
+
summary: "the clipboard, for later",
|
|
729
|
+
},
|
|
730
|
+
{ command: "jump 5 · jump chorus", summary: "move the playhead" },
|
|
731
|
+
{ command: "ctrl-t · tape", summary: "every track across the bars" },
|
|
732
|
+
{
|
|
733
|
+
command: "section chorus 9-16",
|
|
734
|
+
summary: "name bars · section loop chorus",
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
command: "form verse verse chorus",
|
|
738
|
+
summary: "song form · form print lays it out",
|
|
739
|
+
},
|
|
740
|
+
{
|
|
741
|
+
command: "build · drop · fill chorus",
|
|
742
|
+
summary: "riser · pre-drop cut · drum fill",
|
|
743
|
+
},
|
|
744
|
+
{ command: "style deep-house 16", summary: "a whole song in a style" },
|
|
745
|
+
],
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* `/help panes`: several terminals on one session (§12). Its rows are the
|
|
750
|
+
* `project · panes` reference group under a heading of its own.
|
|
751
|
+
*/
|
|
752
|
+
function panesLines(width: number): string[] {
|
|
753
|
+
const group = HELP_SECTIONS.find((s) => s.group === "project · panes")!;
|
|
754
|
+
return [
|
|
755
|
+
...sectionLines([{ ...group, group: "panes" }], width),
|
|
756
|
+
"",
|
|
757
|
+
truncate("dawg pane tape in another terminal · guide panes", width),
|
|
758
|
+
];
|
|
759
|
+
}
|
|
760
|
+
|
|
602
761
|
/** Topics `/help <topic>` takes, besides `all` (aliases resolve too). */
|
|
603
762
|
export const HELP_TOPICS = TOPICS;
|
|
604
763
|
|
|
@@ -656,6 +815,8 @@ export function topicSections(id: TopicId): readonly HelpSection[] {
|
|
|
656
815
|
/** One topic's page: its groups, then where else the topic opens. */
|
|
657
816
|
function topicLines(id: TopicId, width: number): string[] {
|
|
658
817
|
if (id === "keys") return [...keysLines(width), ...doors(id, width)];
|
|
818
|
+
if (id === "arrange")
|
|
819
|
+
return [...sectionLines([ARRANGE_PAGE], width), ...doors(id, width)];
|
|
659
820
|
return [...sectionLines(topicSections(id), width), ...doors(id, width)];
|
|
660
821
|
}
|
|
661
822
|
|
|
@@ -674,6 +835,7 @@ export function helpTopicLines(
|
|
|
674
835
|
return helpLines(width);
|
|
675
836
|
if ((TOPICS as readonly string[]).includes(name))
|
|
676
837
|
return topicLines(name as TopicId, width);
|
|
838
|
+
if (name === "panes" || name === "pane") return panesLines(width);
|
|
677
839
|
const spelled = HELP_COMMAND_ALIASES[name];
|
|
678
840
|
if (spelled) return commandTopic(spelled, width, name);
|
|
679
841
|
const alias = resolveTopic(name);
|
|
@@ -865,11 +1027,25 @@ export const USAGE: Readonly<Record<string, string>> = {
|
|
|
865
1027
|
put: "add <note> at <beat> [for <beats>] · add C4 at 0",
|
|
866
1028
|
remove: "remove <id> · ids show in the transcript",
|
|
867
1029
|
delete: "remove <id> · ids show in the transcript",
|
|
868
|
-
move: "move <id> to <beat>",
|
|
1030
|
+
move: "move <id> to <beat> · move [<track>|all] <a>-<b> to <bar> [insert] · move bass 5-6 to 9",
|
|
869
1031
|
length: "length <id> <beats>",
|
|
870
1032
|
velocity: "velocity <id> <0..1>",
|
|
871
1033
|
vel: "velocity <id> <0..1>",
|
|
872
|
-
bars: "bars
|
|
1034
|
+
bars: "bars <count> (1…256) | bars insert <n> at <bar> | bars remove <a>-<b> · bars 8 · bars insert 2 at 3",
|
|
1035
|
+
copy: "copy [<track>|all] [<a>-<b>|<section>] [to <bar>] [x<N>] [insert|merge] · copy bass 5-6 to 7 x2",
|
|
1036
|
+
paste:
|
|
1037
|
+
"paste [at <bar>] [x<N>] [insert|merge] · paste at 9 · copy bass 5-6 fills the clipboard",
|
|
1038
|
+
reverse: "reverse [<track>|all] [<a>-<b>|<section>] · reverse bass 5-6",
|
|
1039
|
+
jump: "jump <bar>[.<beat>] | <section> · jump 5 · jump 5.3 · jump chorus",
|
|
1040
|
+
loop: "loop <a>-<b> | <section> | next | prev | off · loop 5-6 · loop chorus · loop off",
|
|
1041
|
+
pane: "pane home|play|tape|sound|menu [<track>] [pin|follow [<letter>]] · pane tape · pane sound bass pin",
|
|
1042
|
+
follow: "follow [<letter>] · follow b · unfollow stops",
|
|
1043
|
+
panes: "panes · who has this session open, on what · pane tape",
|
|
1044
|
+
knobs:
|
|
1045
|
+
"knobs [sound|mix|master|tempo|fx <effect>] · knobs mix · knobs fx reverb",
|
|
1046
|
+
mix: "mix · the mixer page · volume drums 0.5 · pan drums -0.2",
|
|
1047
|
+
audio:
|
|
1048
|
+
"audio [out|in <name|default>] | test · audio out default · audio test",
|
|
873
1049
|
extend: "extend <count> bars · extend 4 bars",
|
|
874
1050
|
instrument:
|
|
875
1051
|
"instrument <name> · sine piano pluck bass sawtooth square triangle wavetable kit · pianos: grand upright felt honkytonk prepared · electric: epiano suitcase dyno wurli clav funkclav · synth: supersaw pulse white pink z_square · voices: vocal aah ooh choir chorale khoomei sygyt kargyraa vocoder…",
|
|
@@ -890,7 +1066,8 @@ export const USAGE: Readonly<Record<string, string>> = {
|
|
|
890
1066
|
automation: "automate <lane> at <beat> <value> · automate volume at 0 0.5",
|
|
891
1067
|
hit: "hit <drum> at <beat> · hit kick at 0",
|
|
892
1068
|
pattern: "pattern <drum> <beats...> | every <step> · pattern kick every 1",
|
|
893
|
-
clear:
|
|
1069
|
+
clear:
|
|
1070
|
+
"clear · clear <drum> · clear [<lane>] automation · clear [<track>|all] <a>-<b> · clear bass 5-6",
|
|
894
1071
|
track:
|
|
895
1072
|
"track <name> · track drums · track rm <name> · track move <name> <position> · track rate <0.125..8>|<a>/<b>|off · track phase <beats> · track loop <beats> · track phasing <beats> [over <beats>]",
|
|
896
1073
|
tracks: "tracks",
|
|
@@ -977,6 +1154,7 @@ export const USAGE: Readonly<Record<string, string>> = {
|
|
|
977
1154
|
"master <unit> on|off|preset <name>|<param> <value> · master streaming|club|loud · master target -14 · master measure · master off",
|
|
978
1155
|
try: "/try <sound command> · /try fx reverb mix 0.6",
|
|
979
1156
|
play: "/play [on|off|degrees|in-key|chromatic] · ctrl-p play mode · i toggles degrees",
|
|
1157
|
+
tape: "/tape [on|off] · ctrl-t · every track across the bars · c copy · x cut · v paste · \\ loop",
|
|
980
1158
|
meter:
|
|
981
1159
|
"meter <1..16> · meter 3 · meter 7/8 [at bar <n>] · meter remove bar <n> · meter clear",
|
|
982
1160
|
art: EXPRESSION_USAGE.art,
|
|
@@ -990,9 +1168,9 @@ export const USAGE: Readonly<Record<string, string>> = {
|
|
|
990
1168
|
velcurve: EXPRESSION_USAGE.velcurve,
|
|
991
1169
|
humanize: EXPRESSION_USAGE.humanize,
|
|
992
1170
|
section:
|
|
993
|
-
"section [mark] <name> <a>-<b> | add [<name>] [<n>] | dup | move <name> to <bar> | rename <name> to <new> | delete | unmark | mute | vary | reset | loop <name>|off | jump <name> · section chorus 9-16",
|
|
1171
|
+
"section [mark] <name> <a>-<b> | add [<name>] [<n>] | dup | move <name> to <bar> | rename <name> to <new> | delete | unmark | mute | vary | reset | loop <name>|off | jump <name> | split <name> at <bar> | join <name> · section chorus 9-16",
|
|
994
1172
|
sections: "section · lists sections, the form and the loop",
|
|
995
|
-
form: "form <section…> | off | bake · form verse verse chorus verse · form verse chorus*2",
|
|
1173
|
+
form: "form <section…> | off | bake (print) · form verse verse chorus verse · form verse chorus*2",
|
|
996
1174
|
build:
|
|
997
1175
|
"build [into <section> | <section> | <a>-<b>] [<n> bars] [riser] [roll] [sweep] [uplifter] · build into chorus",
|
|
998
1176
|
drop: "drop [<section> | at <bar>] [cut <beats>] [no impact] · drop chorus",
|
package/src/commands/history.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
|
17
17
|
* every newer event. Events whose rewind was compacted away, and events that
|
|
18
18
|
* did not change the composition (transport), take no part.
|
|
19
19
|
*/
|
|
20
|
-
import
|
|
20
|
+
import { DiffError, diffScores } from "../../core/diff.ts";
|
|
21
|
+
import {
|
|
22
|
+
scoreFromJSON,
|
|
23
|
+
type ScoreOperation,
|
|
24
|
+
type TrackScore,
|
|
25
|
+
} from "../../core/score.ts";
|
|
26
|
+
import { rebaseOperations } from "../session/rebase.ts";
|
|
21
27
|
import { musicalReceipt } from "../session/receipt.ts";
|
|
22
28
|
import {
|
|
23
29
|
isIdentityRewind,
|
|
@@ -29,6 +35,9 @@ export type HistoryEvent = Readonly<{
|
|
|
29
35
|
revision: number;
|
|
30
36
|
kind: string;
|
|
31
37
|
rewind?: Rewind | undefined;
|
|
38
|
+
payload?: unknown;
|
|
39
|
+
/** Who wrote it; per-pane undo keys on `clientId` (design §12.6). */
|
|
40
|
+
actor?: Readonly<{ clientId: string }> | undefined;
|
|
32
41
|
}>;
|
|
33
42
|
|
|
34
43
|
export type HistoryTarget = Readonly<{
|
|
@@ -41,31 +50,133 @@ export type HistoryTarget = Readonly<{
|
|
|
41
50
|
export const UNDO_KIND = "score.undo";
|
|
42
51
|
export const REDO_KIND = "score.redo";
|
|
43
52
|
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Replays the log into undo and redo stacks of event indexes, over the
|
|
55
|
+
* events `include` admits. An undo or redo event pops its stack only when
|
|
56
|
+
* it stepped that stack's top (its payload names the revision); otherwise
|
|
57
|
+
* it is an ordinary edit there. So a pane's own undo is an edit in the
|
|
58
|
+
* global history (`undo all` can undo it), and an `undo all` that undid
|
|
59
|
+
* another pane's edit is an edit in this pane's history. Old events without
|
|
60
|
+
* the payload revision pop unconditionally, as they always did.
|
|
61
|
+
*/
|
|
62
|
+
function historyStacks(
|
|
46
63
|
events: readonly HistoryEvent[],
|
|
47
|
-
|
|
48
|
-
):
|
|
64
|
+
include: (event: HistoryEvent) => boolean,
|
|
65
|
+
): { undo: number[]; redo: number[] } {
|
|
49
66
|
const undo: number[] = [];
|
|
50
67
|
const redo: number[] = [];
|
|
68
|
+
const steps = (stack: number[], event: HistoryEvent, key: string) => {
|
|
69
|
+
const top = stack.at(-1);
|
|
70
|
+
if (top === undefined) return false;
|
|
71
|
+
const named = (event.payload as Record<string, unknown> | null)?.[key];
|
|
72
|
+
return typeof named !== "number" || named === events[top]!.revision;
|
|
73
|
+
};
|
|
51
74
|
events.forEach((event, index) => {
|
|
52
|
-
if (!changesComposition(event)) return;
|
|
53
|
-
if (event.kind === UNDO_KIND) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
75
|
+
if (!changesComposition(event) || !include(event)) return;
|
|
76
|
+
if (event.kind === UNDO_KIND && steps(undo, event, "undoneRevision")) {
|
|
77
|
+
undo.pop();
|
|
78
|
+
redo.push(index);
|
|
79
|
+
} else if (
|
|
80
|
+
event.kind === REDO_KIND &&
|
|
81
|
+
steps(redo, event, "redoneRevision")
|
|
82
|
+
) {
|
|
83
|
+
redo.pop();
|
|
84
|
+
undo.push(index);
|
|
57
85
|
} else {
|
|
58
86
|
undo.push(index);
|
|
59
87
|
redo.length = 0;
|
|
60
88
|
}
|
|
61
89
|
});
|
|
62
|
-
|
|
90
|
+
return { undo, redo };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** `undo all` / `redo all`: the global history, whoever made each edit. */
|
|
94
|
+
export function historyTarget(
|
|
95
|
+
composition: unknown,
|
|
96
|
+
events: readonly HistoryEvent[],
|
|
97
|
+
direction: "undo" | "redo",
|
|
98
|
+
): HistoryTarget | undefined {
|
|
99
|
+
const stacks = historyStacks(events, () => true);
|
|
100
|
+
const top = stacks[direction].at(-1);
|
|
63
101
|
if (top === undefined) return undefined;
|
|
64
102
|
const restored = rewindComposition(composition, events, top);
|
|
65
103
|
if (restored === undefined) return undefined;
|
|
66
104
|
return { revision: events[top]!.revision, composition: restored };
|
|
67
105
|
}
|
|
68
106
|
|
|
107
|
+
export type PaneStep =
|
|
108
|
+
| Readonly<{ ok: true; revision: number; next: TrackScore }>
|
|
109
|
+
| Readonly<{
|
|
110
|
+
ok: false;
|
|
111
|
+
revision: number;
|
|
112
|
+
/** Why the inverse cannot land (rebase reason or session-wide). */
|
|
113
|
+
reason: string;
|
|
114
|
+
/** Other clients that changed the composition since, oldest first. */
|
|
115
|
+
others: readonly string[];
|
|
116
|
+
}>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Per-pane undo and redo (design §12.6): the newest edit `clientId` made,
|
|
120
|
+
* inverted as score operations and rebased onto the current score with the
|
|
121
|
+
* same touch rules as agent intents. Another pane's later edit to the same
|
|
122
|
+
* note or track property refuses with a reason; unrelated edits stay.
|
|
123
|
+
* Undefined when this pane has nothing to step.
|
|
124
|
+
*/
|
|
125
|
+
export function paneHistoryStep(
|
|
126
|
+
composition: unknown,
|
|
127
|
+
events: readonly HistoryEvent[],
|
|
128
|
+
direction: "undo" | "redo",
|
|
129
|
+
clientId: string,
|
|
130
|
+
): PaneStep | undefined {
|
|
131
|
+
const stacks = historyStacks(
|
|
132
|
+
events,
|
|
133
|
+
(event) => event.actor?.clientId === clientId,
|
|
134
|
+
);
|
|
135
|
+
const top = stacks[direction].at(-1);
|
|
136
|
+
if (top === undefined) return undefined;
|
|
137
|
+
const revision = events[top]!.revision;
|
|
138
|
+
const others = [
|
|
139
|
+
...new Set(
|
|
140
|
+
events
|
|
141
|
+
.slice(top + 1)
|
|
142
|
+
.filter(
|
|
143
|
+
(event) =>
|
|
144
|
+
changesComposition(event) && event.actor?.clientId !== clientId,
|
|
145
|
+
)
|
|
146
|
+
.map((event) => event.actor?.clientId ?? ""),
|
|
147
|
+
),
|
|
148
|
+
];
|
|
149
|
+
const refuse = (reason: string): PaneStep => ({
|
|
150
|
+
ok: false,
|
|
151
|
+
revision,
|
|
152
|
+
reason,
|
|
153
|
+
others,
|
|
154
|
+
});
|
|
155
|
+
const before = rewindComposition(composition, events, top);
|
|
156
|
+
const after =
|
|
157
|
+
top + 1 === events.length
|
|
158
|
+
? composition
|
|
159
|
+
: rewindComposition(composition, events, top + 1);
|
|
160
|
+
if (before === undefined || after === undefined)
|
|
161
|
+
return refuse("history was compacted");
|
|
162
|
+
let operations: readonly ScoreOperation[];
|
|
163
|
+
let base: TrackScore;
|
|
164
|
+
let current: TrackScore;
|
|
165
|
+
try {
|
|
166
|
+
base = scoreFromJSON(after);
|
|
167
|
+
current = scoreFromJSON(composition);
|
|
168
|
+
operations = diffScores(base, scoreFromJSON(before));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
if (error instanceof DiffError) return refuse("session-wide");
|
|
171
|
+
return refuse(error instanceof Error ? error.message : String(error));
|
|
172
|
+
}
|
|
173
|
+
if (operations.length === 0) return refuse("session-wide");
|
|
174
|
+
const result = rebaseOperations(base, current, operations);
|
|
175
|
+
return result.ok
|
|
176
|
+
? { ok: true, revision, next: result.next }
|
|
177
|
+
: refuse(result.reason);
|
|
178
|
+
}
|
|
179
|
+
|
|
69
180
|
function changesComposition(event: HistoryEvent): boolean {
|
|
70
181
|
return event.rewind !== undefined && !isIdentityRewind(event.rewind);
|
|
71
182
|
}
|
package/src/commands/keys.ts
CHANGED
|
@@ -210,6 +210,23 @@ export function keysRowMismatch(
|
|
|
210
210
|
return `${instrument} has no ${wrong.join(" ")} (${familiesReading(wrong[0]!).join("/")} row); its parameters: ${rows.join(" ")}`;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
/**
|
|
214
|
+
* `keys record` / `keys record replace` / `keys record off` (op1-ux §4.7):
|
|
215
|
+
* arm recording in PLAY, overdub or replace each pass. Not a score write
|
|
216
|
+
* of its own; each pass the take commits is.
|
|
217
|
+
*/
|
|
218
|
+
export function parseRecordCommand(
|
|
219
|
+
prompt: string,
|
|
220
|
+
): "overdub" | "replace" | "off" | undefined {
|
|
221
|
+
const match = prompt
|
|
222
|
+
.trim()
|
|
223
|
+
.match(/^\/?keys\s+record(?:\s+(overdub|replace|off))?$/i);
|
|
224
|
+
if (!match) return undefined;
|
|
225
|
+
return (match[1]?.toLowerCase() ?? "overdub") as
|
|
226
|
+
"overdub" | "replace" | "off";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Parse one keys / piano / organ command. */
|
|
213
230
|
export function parseKeysCommand(prompt: string): KeysCommand | undefined {
|
|
214
231
|
if (prompt.length > 1_024) return undefined;
|
|
215
232
|
let words = prompt.trim().toLowerCase().split(/\s+/);
|
package/src/commands/parses.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { TrackScore } from "../../core/score.ts";
|
|
8
8
|
import { parsePrompt } from "../agent/ops.ts";
|
|
9
9
|
import { parseSectionCommand } from "./arrange.ts";
|
|
10
|
+
import { parseRangeCommand } from "./range.ts";
|
|
10
11
|
import { parseAutotuneCommand } from "./autotune.ts";
|
|
11
12
|
import { parseClipCommand, parseLyricsCommand } from "./clips.ts";
|
|
12
13
|
import { parsePatternCommand } from "./drums.ts";
|
|
@@ -17,7 +18,7 @@ import { parseFitCommand } from "./fit.ts";
|
|
|
17
18
|
import { parseFormantCommand, parseVowelCommand } from "./formant.ts";
|
|
18
19
|
import { parseFxCommand } from "./fx.ts";
|
|
19
20
|
import { parseGranularCommand } from "./granular.ts";
|
|
20
|
-
import { parseKeysCommand } from "./keys.ts";
|
|
21
|
+
import { parseKeysCommand, parseRecordCommand } from "./keys.ts";
|
|
21
22
|
import { parseMasterCommand } from "./master.ts";
|
|
22
23
|
import { parseModalCommand } from "./modal.ts";
|
|
23
24
|
import { parseMusicCommand } from "./music.ts";
|
|
@@ -85,6 +86,9 @@ export function commandParses(text: string, score: TrackScore): boolean {
|
|
|
85
86
|
|
|
86
87
|
function parsers(score: TrackScore): ((text: string) => unknown)[] {
|
|
87
88
|
return [
|
|
89
|
+
function parseRange(value: string) {
|
|
90
|
+
return parseRangeCommand(value, score);
|
|
91
|
+
},
|
|
88
92
|
parsePrompt,
|
|
89
93
|
parseMusicCommand,
|
|
90
94
|
parseEditCommand,
|
|
@@ -93,6 +97,7 @@ function parsers(score: TrackScore): ((text: string) => unknown)[] {
|
|
|
93
97
|
parseSynthCommand,
|
|
94
98
|
parseStringCommand,
|
|
95
99
|
parseGranularCommand,
|
|
100
|
+
parseRecordCommand,
|
|
96
101
|
parseKeysCommand,
|
|
97
102
|
parseExpressionCommand,
|
|
98
103
|
parseMasterCommand,
|