@hraness/dawg 0.6.0 → 0.6.1
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 +34 -0
- package/DAWG.md +338 -51
- package/README.md +4 -4
- package/core/chords.ts +492 -4
- package/core/diff.ts +5 -0
- package/core/expression.ts +143 -6
- package/core/fx.ts +258 -6
- package/core/granular.ts +91 -0
- package/core/instruments.ts +161 -0
- package/core/keys.ts +573 -4
- package/core/resonators.ts +291 -3
- package/core/score.ts +349 -3
- package/core/sdk/print.ts +78 -3
- package/core/sdk/v1.ts +1320 -36
- package/core/sections.ts +12 -4
- package/core/strings.ts +237 -2
- package/core/winds.ts +652 -0
- package/guides/automation.md +1 -0
- package/guides/chords.md +3 -1
- package/guides/effects.md +3 -2
- package/guides/media.md +1 -1
- package/guides/performance.md +3 -0
- package/guides/resample.md +26 -0
- package/guides/sounds.md +7 -6
- package/package.json +1 -1
- package/src/agent/agent.ts +8 -2
- package/src/agent/chord-tools.ts +153 -1
- package/src/agent/expression-tools.ts +91 -0
- package/src/agent/ops.ts +4 -1
- package/src/agent/preview-tool.ts +2 -1
- package/src/agent/resample-tool.ts +131 -0
- package/src/agent/tools.ts +200 -15
- package/src/agent/xcb-agent.ts +8 -2
- package/src/audio/arrange.ts +21 -0
- package/src/audio/dsp/envelope.ts +161 -0
- package/src/audio/dsp/shift.ts +256 -0
- package/src/audio/effects/chain.ts +18 -3
- package/src/audio/effects/common.ts +14 -0
- package/src/audio/effects/convolution.ts +106 -1
- package/src/audio/effects/gaze.ts +354 -0
- package/src/audio/engine.ts +46 -5
- package/src/audio/fit.ts +45 -0
- package/src/audio/granular.ts +352 -21
- package/src/audio/instrument-check.ts +9 -5
- package/src/audio/instruments.ts +32 -3
- package/src/audio/keys/electric.ts +420 -0
- package/src/audio/keys/engine.ts +143 -19
- package/src/audio/keys/organ.ts +1335 -0
- package/src/audio/keys/piano.ts +50 -6
- package/src/audio/keys/sympathetic.ts +127 -0
- package/src/audio/live-worker.ts +3 -1
- package/src/audio/live.ts +136 -9
- package/src/audio/preview.ts +15 -1
- package/src/audio/resample.ts +285 -0
- package/src/audio/resonators.ts +10 -2
- package/src/audio/sampler.ts +168 -21
- package/src/audio/samples.ts +26 -1
- package/src/audio/strings/body.ts +29 -16
- package/src/audio/strings/bow.ts +656 -0
- package/src/audio/strings/engine.ts +266 -20
- package/src/audio/strings/measure.test-helpers.ts +2 -0
- package/src/audio/wav.ts +119 -19
- package/src/audio/winds/engine.ts +302 -0
- package/src/audio/winds/filters.ts +153 -0
- package/src/audio/winds/pitch.ts +104 -0
- package/src/audio/winds/trim.ts +80 -0
- package/src/audio/winds/trims.ts +917 -0
- package/src/audio/winds/voice.ts +479 -0
- package/src/commands/expression.ts +103 -22
- package/src/commands/fx.ts +4 -1
- package/src/commands/granular.ts +7 -2
- package/src/commands/help.ts +91 -8
- package/src/commands/keys.ts +287 -11
- package/src/commands/modal.ts +67 -4
- package/src/commands/resample.ts +281 -0
- package/src/commands/rig.ts +5 -1
- package/src/commands/sample.ts +49 -2
- package/src/commands/shift.ts +119 -0
- package/src/commands/string.ts +28 -2
- package/src/commands/strum.ts +473 -0
- package/src/commands/wind.ts +244 -0
- package/src/main.ts +129 -7
- package/src/project/check.ts +5 -1
- package/src/render.ts +16 -0
- package/src/session/presence.ts +16 -3
- package/src/tui/audition.ts +1 -1
- package/src/tui/menu.ts +637 -35
- package/src/tui/modal-menu.ts +34 -7
- package/src/tui/performance-menu.ts +42 -0
- package/src/tui/play-chords.ts +104 -3
- package/src/tui/play-mode.ts +1 -0
- package/src/tui/play-session.ts +34 -9
- package/src/tui/wind-menu.ts +144 -0
package/src/agent/tools.ts
CHANGED
|
@@ -14,6 +14,15 @@ import {
|
|
|
14
14
|
type TrackModal,
|
|
15
15
|
} from "../../core/resonators.ts";
|
|
16
16
|
import { applyModalCommand, type ModalCommand } from "../commands/modal.ts";
|
|
17
|
+
import { applyWindCommand, type WindCommand } from "../commands/wind.ts";
|
|
18
|
+
import {
|
|
19
|
+
type TrackWind,
|
|
20
|
+
WIND_ALIASES,
|
|
21
|
+
WIND_PARAMS,
|
|
22
|
+
WIND_PRESET_NAMES,
|
|
23
|
+
windParamName,
|
|
24
|
+
windPresetFor,
|
|
25
|
+
} from "../../core/winds.ts";
|
|
17
26
|
import {
|
|
18
27
|
AUTOMATION_PARAMETERS,
|
|
19
28
|
automationPoints,
|
|
@@ -46,9 +55,11 @@ import {
|
|
|
46
55
|
} from "../commands/keys.ts";
|
|
47
56
|
import {
|
|
48
57
|
KEYS_PRESETS,
|
|
58
|
+
ORGAN_ALIASES,
|
|
49
59
|
KEYS_SIMPLE,
|
|
50
60
|
isKeysPreset,
|
|
51
61
|
keysParamName,
|
|
62
|
+
pianoWrite,
|
|
52
63
|
} from "../../core/keys.ts";
|
|
53
64
|
import {
|
|
54
65
|
setSampleControls,
|
|
@@ -96,6 +107,7 @@ import { DRUM_TOOLS } from "./drum-tools.ts";
|
|
|
96
107
|
import { TIME_TOOLS } from "./time-tools.ts";
|
|
97
108
|
import { SECTION_TOOLS } from "./section-tools.ts";
|
|
98
109
|
import { GRANULAR_TOOLS } from "./granular-tools.ts";
|
|
110
|
+
import { RESAMPLE_TOOLS } from "./resample-tool.ts";
|
|
99
111
|
import type { MediaResult, MediaRunContext } from "../media/types.ts";
|
|
100
112
|
import { instrumentPatch, pitchToMidi } from "./ops.ts";
|
|
101
113
|
import { TUNING_LIMITS } from "../../core/tuning.ts";
|
|
@@ -474,7 +486,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
474
486
|
{
|
|
475
487
|
name: "set_instrument",
|
|
476
488
|
description:
|
|
477
|
-
"Change a track's instrument voice. Mallets and bells (modal): modal (marimba) vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani
|
|
489
|
+
"Change a track's instrument voice. Mallets and bells (modal): modal (marimba) vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani, gamelan saron gangsa bonang …; set_modal shapes them. Electric keys: epiano suitcase dyno wurli clav funkclav (set_keys). Winds and brass (wind engine): flute recorder clarinet oboe bassoon sax trumpet trombone tuba horn …; set_wind shapes them.",
|
|
478
490
|
parameters: {
|
|
479
491
|
type: "object",
|
|
480
492
|
properties: {
|
|
@@ -490,7 +502,8 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
490
502
|
const track = context.score.tracks.find((t) => t.id === trackId);
|
|
491
503
|
if (track && typeof args.instrument === "string") {
|
|
492
504
|
const word = args.instrument.toLowerCase();
|
|
493
|
-
|
|
505
|
+
// `rhodes`, `wurlitzer`, `clavinet` name their electric presets.
|
|
506
|
+
const preset = pianoWrite(word)?.preset ?? word;
|
|
494
507
|
if (isKeysPreset(preset)) {
|
|
495
508
|
const patch = keysPresetPatch(track, preset);
|
|
496
509
|
return {
|
|
@@ -513,7 +526,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
513
526
|
summary:
|
|
514
527
|
patch.instrument === "marimba" && !patch.modal
|
|
515
528
|
? `${trackId} → marimba (legacy tone) · set_modal preset marimba for the mallet engine`
|
|
516
|
-
: `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.instrument}`,
|
|
529
|
+
: `${trackId} → ${patch.string?.preset ?? patch.modal?.preset ?? patch.wind?.preset ?? patch.instrument}`,
|
|
517
530
|
};
|
|
518
531
|
},
|
|
519
532
|
},
|
|
@@ -587,7 +600,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
587
600
|
parameter: {
|
|
588
601
|
type: "string",
|
|
589
602
|
description:
|
|
590
|
-
"volume, pan, filter, resonance, delay-feedback, delay-mix,
|
|
603
|
+
"volume, pan, filter, resonance, delay-feedback, delay-mix, <effect>-<param>, or grain-<param> on a granular track",
|
|
591
604
|
},
|
|
592
605
|
mode: { type: "string", enum: ["replace", "merge"] },
|
|
593
606
|
points: {
|
|
@@ -713,7 +726,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
713
726
|
{
|
|
714
727
|
name: "set_rig",
|
|
715
728
|
description:
|
|
716
|
-
"Guitar rig on a track (stomp → amp head with noise gate → speaker cab, before tremolo). rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) or reset removes it; stomp/head/cab set stage params (stomp type fuzz|face|od|rat|octave gain tone level; head type clean|chime|crunch|lead|high|solid|bass gain bass mid treble presence master sag gate(dB); cab type 1x12|2x12|4x12|1x10|open|8x10|1x15|di mic), or null removes a stage. Heads are level-matched. `amp` is Strudel gain, not this.",
|
|
729
|
+
"Guitar rig on a track (stomp → amp head with noise gate → speaker cab, before tremolo). rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt, and the shoegaze rigs shoegaze glide dreampop swell ebow that add wobble/bloom/swell/double and a long reverb) or reset removes it; stomp/head/cab set stage params (stomp type fuzz|face|od|rat|octave gain tone level; head type clean|chime|crunch|lead|high|solid|bass gain bass mid treble presence master sag gate(dB); cab type 1x12|2x12|4x12|1x10|open|8x10|1x15|di mic), or null removes a stage. Heads are level-matched. `amp` is Strudel gain, not this.",
|
|
717
730
|
parameters: {
|
|
718
731
|
type: "object",
|
|
719
732
|
properties: {
|
|
@@ -844,7 +857,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
844
857
|
{
|
|
845
858
|
name: "set_string",
|
|
846
859
|
description:
|
|
847
|
-
"Make a track a plucked string (physical model): preset picks the instrument; params override it (ring s, bright, damp, pos, mute, buzz = jawari, body, sym = sympathetic strings, stiff, exciter pick|finger|hammer|noise); null unsets one. reset keeps the preset and drops overrides; off returns the track to a plain pluck voice.",
|
|
860
|
+
"Make a track a plucked, struck or bowed string (physical model). Bowed presets (exciter bow): violin viola cello contrabass fiddle erhu kamancheh, sections violins violas cellos contrabasses, pizz trem. preset picks the instrument; params override it (ring s, bright, damp, pos, mute, buzz = jawari, body, sym = sympathetic strings, stiff, exciter pick|finger|hammer|noise|bow); null unsets one. Bowed presets take pressure speed attack vib vibmod vibdelay tremhz sord dyn. reset keeps the preset and drops overrides; off returns the track to a plain pluck voice.",
|
|
848
861
|
parameters: {
|
|
849
862
|
type: "object",
|
|
850
863
|
properties: {
|
|
@@ -886,17 +899,25 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
886
899
|
},
|
|
887
900
|
{
|
|
888
901
|
name: "set_keys",
|
|
889
|
-
description: `Shape a modelled piano: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand.`,
|
|
902
|
+
description: `Shape a modelled piano, electric keys (epiano wurli clav) or organ: preset (instrument, keys and its effects), params ${KEYS_SIMPLE.join(" ")}… (pianos: sym 0.5 adds sympathetic bloom under the sustain pedal, good for Chopin and Debussy; epiano: bark bell tone vibe; wurli: trem; clav: pickup mute; null unsets; DAWG.md lists all), or reset. Stored "piano" stays legacy; set_instrument piano writes grand. Organs (not "organ", a legacy synth): presets tonewheel combo pipe… (aliases hammond b3 farfisa church); drawbars "888800008", registers "08880", stops (plenum, flute8…), rotary; params perc percdecay percvol click scanner drive (tonewheel), voice vib vibmod drive (combo), chiff wind trem (pipe). A row the family does not read is refused.`,
|
|
890
903
|
parameters: {
|
|
891
904
|
type: "object",
|
|
892
905
|
properties: {
|
|
893
906
|
trackId: trackIdSchema,
|
|
894
|
-
preset: {
|
|
907
|
+
preset: {
|
|
908
|
+
type: "string",
|
|
909
|
+
enum: [...Object.keys(KEYS_PRESETS), ...Object.keys(ORGAN_ALIASES)],
|
|
910
|
+
},
|
|
895
911
|
reset: { type: "boolean" },
|
|
896
912
|
params: {
|
|
897
913
|
type: "object",
|
|
898
914
|
additionalProperties: { type: ["number", "string", "null"] },
|
|
899
915
|
},
|
|
916
|
+
// f061-organ
|
|
917
|
+
drawbars: { type: "string", pattern: "^[0-8]{9}$" },
|
|
918
|
+
registers: { type: "string", pattern: "^[0-8]{5}$" },
|
|
919
|
+
stops: { type: ["array", "string"], items: { type: "string" } },
|
|
920
|
+
rotary: { type: "string", enum: ["slow", "fast", "stop"] },
|
|
900
921
|
},
|
|
901
922
|
additionalProperties: false,
|
|
902
923
|
},
|
|
@@ -935,7 +956,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
935
956
|
{
|
|
936
957
|
name: "set_modal",
|
|
937
958
|
description:
|
|
938
|
-
"Mallets and bells on the modal engine: preset (marimba vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) switches the voice and keeps overrides; mallet yarn|cord|rubber|plastic|brass; params sets MODAL_PARAMS (hardness position ring tilt release damp motor motordepth ombak buzz click strikebend strikedecay gain), null returns one to the preset; reset clears overrides. Turns the track into instrument modal.",
|
|
959
|
+
"Mallets and bells on the modal engine: preset (marimba vibes xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) switches the voice and keeps overrides (gamelan: crotales musicbox toypiano saron demung slenthem gangsa gender bonang kenong kethuk kempul; frame drums daf bodhran tabla); mallet yarn|cord|rubber|plastic|brass; params sets MODAL_PARAMS (hardness position ring tilt release damp motor motordepth ombak buzz click strikebend strikedecay gain), null returns one to the preset; pair names a partner modal track: this track (pengisep) sounds ombak Hz above it and the partner (pengumbang) gets ombak 0, null unpairs; reset clears overrides. Turns the track into instrument modal.",
|
|
939
960
|
parameters: {
|
|
940
961
|
type: "object",
|
|
941
962
|
properties: {
|
|
@@ -945,6 +966,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
945
966
|
enum: [...MODAL_PRESET_NAMES, ...Object.keys(MODAL_ALIASES)],
|
|
946
967
|
},
|
|
947
968
|
mallet: { type: "string", enum: [...MODAL_MALLET_NAMES] },
|
|
969
|
+
pair: { type: ["string", "null"] },
|
|
948
970
|
reset: { type: "boolean" },
|
|
949
971
|
params: {
|
|
950
972
|
type: "object",
|
|
@@ -974,6 +996,11 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
974
996
|
throw new ToolArgumentError("mallet must be a string");
|
|
975
997
|
values.mallet = args.mallet;
|
|
976
998
|
}
|
|
999
|
+
if (args.pair !== undefined) {
|
|
1000
|
+
if (args.pair !== null && typeof args.pair !== "string")
|
|
1001
|
+
throw new ToolArgumentError("pair must be a track id or null");
|
|
1002
|
+
values.pair = args.pair;
|
|
1003
|
+
}
|
|
977
1004
|
if (args.params !== undefined) {
|
|
978
1005
|
if (
|
|
979
1006
|
typeof args.params !== "object" ||
|
|
@@ -1019,9 +1046,20 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1019
1046
|
messages.push(result.message);
|
|
1020
1047
|
}
|
|
1021
1048
|
const next = score.tracks.find((t) => t.id === trackId)!;
|
|
1049
|
+
// A pair also sets the partner (pengumbang) to ombak 0.
|
|
1050
|
+
const partners = score.tracks.filter(
|
|
1051
|
+
(t) =>
|
|
1052
|
+
t.id !== trackId &&
|
|
1053
|
+
t.modal !== context.score.tracks.find((o) => o.id === t.id)?.modal,
|
|
1054
|
+
);
|
|
1022
1055
|
return {
|
|
1023
1056
|
kind: "score",
|
|
1024
1057
|
operations: [
|
|
1058
|
+
...partners.map((t) => ({
|
|
1059
|
+
type: "updateTrack" as const,
|
|
1060
|
+
trackId: t.id,
|
|
1061
|
+
patch: { modal: t.modal ?? null },
|
|
1062
|
+
})),
|
|
1025
1063
|
{
|
|
1026
1064
|
type: "updateTrack",
|
|
1027
1065
|
trackId,
|
|
@@ -1033,10 +1071,111 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1033
1071
|
};
|
|
1034
1072
|
},
|
|
1035
1073
|
},
|
|
1074
|
+
{
|
|
1075
|
+
name: "set_wind",
|
|
1076
|
+
description:
|
|
1077
|
+
"Winds and brass on the wind engine (breath-driven waveguides): preset (flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn) switches the voice and keeps overrides; params sets WIND_PARAMS (breath bright mute players growl noise attack release vib vibmod reed stopped wah wahenv flutter gain model), null returns one to the preset; players 2..8 is a section that spreads over chord tones; reset clears overrides; off returns to the legacy wind tone. Lines slur by default (a note that starts while one note is held is not re-tongued). Turns the track into the wind engine.",
|
|
1078
|
+
parameters: {
|
|
1079
|
+
type: "object",
|
|
1080
|
+
properties: {
|
|
1081
|
+
trackId: trackIdSchema,
|
|
1082
|
+
preset: {
|
|
1083
|
+
type: "string",
|
|
1084
|
+
enum: [...WIND_PRESET_NAMES, ...Object.keys(WIND_ALIASES)],
|
|
1085
|
+
},
|
|
1086
|
+
reset: { type: "boolean" },
|
|
1087
|
+
off: { type: "boolean" },
|
|
1088
|
+
params: {
|
|
1089
|
+
type: "object",
|
|
1090
|
+
additionalProperties: {
|
|
1091
|
+
type: ["number", "string", "boolean", "null"],
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
additionalProperties: false,
|
|
1096
|
+
},
|
|
1097
|
+
plan(args, context) {
|
|
1098
|
+
const trackId = targetTrack(args, context);
|
|
1099
|
+
const commands: WindCommand[] = [];
|
|
1100
|
+
if (args.off === true) commands.push({ type: "wind-off" });
|
|
1101
|
+
if (args.reset === true) commands.push({ type: "wind-reset" });
|
|
1102
|
+
if (args.preset !== undefined) {
|
|
1103
|
+
const preset =
|
|
1104
|
+
typeof args.preset === "string"
|
|
1105
|
+
? windPresetFor(args.preset)
|
|
1106
|
+
: undefined;
|
|
1107
|
+
if (!preset)
|
|
1108
|
+
throw new ToolArgumentError(
|
|
1109
|
+
`preset must be one of ${WIND_PRESET_NAMES.join(", ")}`,
|
|
1110
|
+
);
|
|
1111
|
+
commands.push({ type: "wind-preset", preset });
|
|
1112
|
+
}
|
|
1113
|
+
const values: Record<string, number | string | boolean | null> = {};
|
|
1114
|
+
if (args.params !== undefined) {
|
|
1115
|
+
if (
|
|
1116
|
+
typeof args.params !== "object" ||
|
|
1117
|
+
args.params === null ||
|
|
1118
|
+
Array.isArray(args.params)
|
|
1119
|
+
)
|
|
1120
|
+
throw new ToolArgumentError("params must be an object");
|
|
1121
|
+
for (const [key, value] of Object.entries(args.params)) {
|
|
1122
|
+
const name = windParamName(key);
|
|
1123
|
+
if (!name)
|
|
1124
|
+
throw new ToolArgumentError(
|
|
1125
|
+
`wind has no parameter ${key} (${Object.keys(WIND_PARAMS).join(" ")})`,
|
|
1126
|
+
);
|
|
1127
|
+
if (
|
|
1128
|
+
value !== null &&
|
|
1129
|
+
typeof value !== "number" &&
|
|
1130
|
+
typeof value !== "string" &&
|
|
1131
|
+
typeof value !== "boolean"
|
|
1132
|
+
)
|
|
1133
|
+
throw new ToolArgumentError(
|
|
1134
|
+
`${key} must be a number, string or boolean`,
|
|
1135
|
+
);
|
|
1136
|
+
values[name] = value;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (Object.keys(values).length > 0)
|
|
1140
|
+
commands.push({ type: "wind-set", values });
|
|
1141
|
+
if (commands.length === 0) {
|
|
1142
|
+
// An empty call turns the track into the wind engine.
|
|
1143
|
+
const current = context.score.tracks.find((t) => t.id === trackId);
|
|
1144
|
+
commands.push({
|
|
1145
|
+
type: "wind-preset",
|
|
1146
|
+
preset:
|
|
1147
|
+
current?.instrument === "wind" && current.wind?.preset
|
|
1148
|
+
? current.wind.preset
|
|
1149
|
+
: "flute",
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
let score = context.score;
|
|
1153
|
+
const messages: string[] = [];
|
|
1154
|
+
for (const command of commands) {
|
|
1155
|
+
const result = applyWindCommand(score, trackId, command);
|
|
1156
|
+
if (!result.ok) throw new ToolArgumentError(result.message);
|
|
1157
|
+
if (result.next) score = result.next;
|
|
1158
|
+
messages.push(result.message);
|
|
1159
|
+
}
|
|
1160
|
+
const next = score.tracks.find((t) => t.id === trackId)!;
|
|
1161
|
+
return {
|
|
1162
|
+
kind: "score",
|
|
1163
|
+
operations: [
|
|
1164
|
+
{
|
|
1165
|
+
type: "updateTrack",
|
|
1166
|
+
trackId,
|
|
1167
|
+
patch: { instrument: next.instrument, wind: next.wind ?? null },
|
|
1168
|
+
},
|
|
1169
|
+
],
|
|
1170
|
+
trackId,
|
|
1171
|
+
summary: `${trackId} ${messages.at(-1)}`,
|
|
1172
|
+
};
|
|
1173
|
+
},
|
|
1174
|
+
},
|
|
1036
1175
|
{
|
|
1037
1176
|
name: "set_sample",
|
|
1038
1177
|
description:
|
|
1039
|
-
"Set a sampler voice's Strudel sample controls: begin end gain speed unit(r|c|s) loop loopBegin loopEnd clip(legato) fit loopAt accelerate squiz cut
|
|
1178
|
+
"Set a sampler voice's Strudel sample controls: begin end gain speed unit(r|c|s) loop loopBegin loopEnd clip(legato) fit loopAt accelerate squiz cut, and (0.6.1) shift (semitones, length kept) formant (0 keeps the voice's formants) fadeInTime fadeTime (seconds), vel [lo,hi] (velocity layer) rr (round-robin group). null unsets one.",
|
|
1040
1179
|
parameters: {
|
|
1041
1180
|
type: "object",
|
|
1042
1181
|
properties: {
|
|
@@ -1045,7 +1184,15 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1045
1184
|
params: {
|
|
1046
1185
|
type: "object",
|
|
1047
1186
|
additionalProperties: {
|
|
1048
|
-
|
|
1187
|
+
anyOf: [
|
|
1188
|
+
{ type: ["number", "string", "boolean", "null"] },
|
|
1189
|
+
{
|
|
1190
|
+
type: "array",
|
|
1191
|
+
items: { type: "integer", minimum: 0, maximum: 127 },
|
|
1192
|
+
minItems: 2,
|
|
1193
|
+
maxItems: 2,
|
|
1194
|
+
},
|
|
1195
|
+
],
|
|
1049
1196
|
},
|
|
1050
1197
|
},
|
|
1051
1198
|
},
|
|
@@ -1505,7 +1652,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1505
1652
|
typeof args.instrument === "string"
|
|
1506
1653
|
? args.instrument.toLowerCase()
|
|
1507
1654
|
: "";
|
|
1508
|
-
const preset = word
|
|
1655
|
+
const preset = pianoWrite(word)?.preset ?? word;
|
|
1509
1656
|
if (isKeysPreset(preset)) {
|
|
1510
1657
|
const patch = keysPresetPatch({}, preset);
|
|
1511
1658
|
return {
|
|
@@ -1822,6 +1969,7 @@ export const AGENT_TOOLS: readonly AgentTool[] = Object.freeze([
|
|
|
1822
1969
|
...PACK_TOOLS,
|
|
1823
1970
|
...MASTER_TOOLS,
|
|
1824
1971
|
...GRANULAR_TOOLS,
|
|
1972
|
+
...RESAMPLE_TOOLS,
|
|
1825
1973
|
// Looks tools up at call time, so it can plan any of the above.
|
|
1826
1974
|
previewSoundTool((name) => findAgentTool(name)),
|
|
1827
1975
|
] satisfies AgentTool[]);
|
|
@@ -1969,19 +2117,54 @@ function fxToolCommand(
|
|
|
1969
2117
|
return { type: "fx-set", effect, values };
|
|
1970
2118
|
}
|
|
1971
2119
|
|
|
2120
|
+
/** f061-organ: set_keys drawbars/registers/stops/rotary as keys values. */
|
|
2121
|
+
function organToolValues(
|
|
2122
|
+
args: Record<string, unknown>,
|
|
2123
|
+
): Record<string, string> {
|
|
2124
|
+
const out: Record<string, string> = {};
|
|
2125
|
+
for (const name of ["drawbars", "registers", "rotary"] as const) {
|
|
2126
|
+
const value = args[name];
|
|
2127
|
+
if (value === undefined) continue;
|
|
2128
|
+
if (typeof value !== "string")
|
|
2129
|
+
throw new ToolArgumentError(`keys ${name} must be a string`);
|
|
2130
|
+
out[name] = value;
|
|
2131
|
+
}
|
|
2132
|
+
if (args.stops !== undefined) {
|
|
2133
|
+
const words = Array.isArray(args.stops)
|
|
2134
|
+
? args.stops
|
|
2135
|
+
: typeof args.stops === "string"
|
|
2136
|
+
? args.stops.split(/[\s,+]+/)
|
|
2137
|
+
: undefined;
|
|
2138
|
+
if (!words || !words.every((word) => typeof word === "string"))
|
|
2139
|
+
throw new ToolArgumentError("keys stops must be stop names");
|
|
2140
|
+
out.stops = words.join(" ");
|
|
2141
|
+
}
|
|
2142
|
+
return out;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1972
2145
|
function keysToolCommand(args: Record<string, unknown>): KeysCommand {
|
|
1973
2146
|
if (args.reset === true) return { type: "keys-reset" };
|
|
2147
|
+
const organ = organToolValues(args);
|
|
2148
|
+
const organSet = Object.keys(organ).length > 0;
|
|
1974
2149
|
if (args.preset !== undefined) {
|
|
1975
|
-
|
|
2150
|
+
const preset =
|
|
2151
|
+
typeof args.preset === "string"
|
|
2152
|
+
? (pianoWrite(args.preset)?.preset ?? args.preset)
|
|
2153
|
+
: undefined;
|
|
2154
|
+
if (preset === undefined || !isKeysPreset(preset))
|
|
1976
2155
|
throw new ToolArgumentError(
|
|
1977
2156
|
`keys presets: ${Object.keys(KEYS_PRESETS).join(", ")}`,
|
|
1978
2157
|
);
|
|
1979
|
-
return
|
|
2158
|
+
return organSet
|
|
2159
|
+
? { type: "keys-preset", preset, values: organ }
|
|
2160
|
+
: { type: "keys-preset", preset };
|
|
1980
2161
|
}
|
|
2162
|
+
if (args.params === undefined && organSet)
|
|
2163
|
+
return { type: "keys-set", values: organ };
|
|
1981
2164
|
if (args.params === undefined)
|
|
1982
2165
|
throw new ToolArgumentError("set_keys needs preset, reset, or params");
|
|
1983
2166
|
const params = record(args.params, "params");
|
|
1984
|
-
const values: Record<string, number | string | null> = {};
|
|
2167
|
+
const values: Record<string, number | string | null> = { ...organ };
|
|
1985
2168
|
for (const [name, value] of Object.entries(params)) {
|
|
1986
2169
|
const param = keysParamName(name);
|
|
1987
2170
|
if (!param)
|
|
@@ -2112,6 +2295,7 @@ function instrumentName(value: unknown): {
|
|
|
2112
2295
|
instrument: string;
|
|
2113
2296
|
string?: { preset: string };
|
|
2114
2297
|
modal?: TrackModal;
|
|
2298
|
+
wind?: TrackWind;
|
|
2115
2299
|
} {
|
|
2116
2300
|
const word = typeof value === "string" ? value.trim() : undefined;
|
|
2117
2301
|
if (word === STRING_INSTRUMENT)
|
|
@@ -2124,6 +2308,7 @@ function instrumentName(value: unknown): {
|
|
|
2124
2308
|
patch === undefined ||
|
|
2125
2309
|
(!patch.string &&
|
|
2126
2310
|
!patch.modal &&
|
|
2311
|
+
!patch.wind &&
|
|
2127
2312
|
!(AVAILABLE_INSTRUMENTS as readonly string[]).includes(patch.instrument))
|
|
2128
2313
|
)
|
|
2129
2314
|
throw new ToolArgumentError(
|
package/src/agent/xcb-agent.ts
CHANGED
|
@@ -51,15 +51,21 @@ export const TEXT_AGENT_SYSTEM_PROMPT = [
|
|
|
51
51
|
CHORD_PROCESS,
|
|
52
52
|
"Synth voices: set_synth shapes a synth track with Strudel params (ADSR, lpf/lpenv filter envelopes, fm/fmh, supersaw unison/detune, vib, penv, noise; z_* ZzFX sounds take slide/pitchJump/lfo/zcrush…) or a preset (pad lead pluck bass keys bell…).",
|
|
53
53
|
"Plucked strings: set_string makes a track a physical-model string (nylon steel electric jangle ebass slap upright motown sitar tanpura harpsichord lute oud setar tar santur dulcimer koto harp banjo tres requinto) and overrides params (ring bright damp pos mute buzz body sym). The bare words sitar, ebass, pluck keep their older voices; use set_string for the modelled ones.",
|
|
54
|
-
"
|
|
54
|
+
"Bowed strings: set_string preset violin|viola|cello|contrabass|fiddle|erhu|kamancheh|violins|violas|cellos|contrabasses|pizz|trem plays a bowed physical model (bow params pressure speed attack vib vibmod vibdelay tremhz sord dyn). Overlapping or legato notes slur in one bow. The bare words cello, contrabass and strings keep their older voices; use set_string (or the words violin, viola, fiddle, erhu, violins, cellos) for the bowed ones. Sampler voices take vel [lo, hi] velocity layers and rr round-robin groups via set_sample.",
|
|
55
|
+
"Pianos: set_instrument piano (or grand upright felt honkytonk prepared, ballad, lofi) loads a modelled piano with real hammers, stretched inharmonic strings, dampers and the sustain pedal; set_keys tweaks it (hardness touch decay release felt stretch body…, automate as keys-<param> lanes; sym 0.5 adds sympathetic bloom under the sustain pedal for pedalled Romantic and Impressionist playing). A track already stored as instrument piano keeps the legacy tone until you set it again.",
|
|
56
|
+
"Electric keys: set_instrument epiano (rhodes; suitcase stereo vibe, dyno bright 80s), wurli, clav (funkclav); set_keys tweaks bark bell tone vibe (epiano), trem (wurli), pickup neck|bridge|both|out and mute (clav). Soft and sostenuto pedals: set_piano_pedals on any modelled piano.",
|
|
57
|
+
'Organs: set_instrument tonewheel (hammond b3 gospel jazzorgan), combo (farfisa vox) or pipe (church flutes cornet reeds celeste) loads a modelled organ; instrument organ stays the legacy sine. set_keys drawbars "888800008" (16\' to 1\'), registers "08880" (combo), stops ["principal8","octave4"] or plenum|flutes|cornet|reeds|strings|full (pipe), rotary slow|fast|stop, params perc 3rd percdecay percvol click scanner c3 drive chiff wind trem (each family takes only its own rows); automate keys-rotary and keys-drive lanes.',
|
|
55
58
|
"Mallets and bells: set_instrument vibes (or marimba via modal, xylophone glock celesta chimes kalimba mbira steelpan bowl gong timpani) plays the modal engine; set_modal changes mallet (yarn…brass), hardness, position, ring, damp, motor/motordepth (vibes tremolo), buzz, click. Plain instrument marimba stays the legacy tone.",
|
|
59
|
+
"Gamelan: set_modal preset saron demung slenthem gangsa gender bonang kenong kethuk kempul (gong too) with tuning slendro or pelog; for ombak (the shimmer) put the same part on two gangsa tracks and set_modal pair <other track> on one: it sounds ombak Hz (default 6) above its partner. Frame drums: daf bodhran tabla.",
|
|
60
|
+
"Winds and brass: set_instrument flute recorder whistle ney shakuhachi panpipe suling bansuri clarinet bassclarinet oboe bassoon sax altosax barisax trumpet harmon plunger trombone tuba horn plays the wind engine; set_wind changes breath, bright, mute (open straight cup harmon plunger), players (2..8 spreads a section over chord tones), growl, vib. Lines slur unless re-tongued (gaps or articulations). Plain instrument wind stays the legacy tone.",
|
|
56
61
|
"Sampler voices: set_sample sets Strudel sample controls per voice (begin end speed unit loop loopBegin loopEnd clip/legato fit loopAt accelerate squiz cut); splice = slices with fit.",
|
|
57
62
|
"Fitting samples (0.6): fit_sample sets a voice's own bpm (or len in beats) so it follows the song's tempo map; fitmode beats for drums and speech (hits stay sharp), tones for pads and vocals (pitch kept), repitch (default) like tape.",
|
|
58
63
|
"Effects (set_fx; set_effects and set_automation): the brief's effects list is the fixed chain order with each effect's simple params. set_fx turns an effect on with good defaults, loads a preset or sets params by dawg or Strudel name. Lanes: volume, pan, filter, resonance, delay-feedback, delay-mix, wt and <effect>-<param> (e.g. autofilter-cutoff).",
|
|
59
64
|
"Guitar rig (set_rig): rig loads a whole rig (clean crunch punk ragged lead metal fuzz octave funk wah bachata spring bassdrive reese jangle alt) on a guitar or bass track; stomp, head and cab tweak the pedal, amp (level-matched, gate in dB) and cabinet. Tracks named jangle punk funk ragged gtr-lead gtr-metal bachata start with a guitar voice and that rig. `amp` is Strudel gain; the guitar amp is head.",
|
|
65
|
+
"Shoegaze: rig shoegaze|glide|dreampop|swell|ebow (or a track named shoegaze); set_fx wobble (tremolo-arm bend: depth cents, rate, drift), bloom (feedback swell on the top note), swell (volume swell: time), double (second take: time, drift, width); reverb ir builtin:reverse|gate|spring. Strumming: set_guitar (tune dadgad, capo, hand, ring), strum_chords (chords or the track's own, strokes folk|pop|punk… or D-DU-UDU, speed ms), write_chords perform guitar.",
|
|
60
66
|
"Previewing: preview_sound renders a track, or candidate sound tool calls (changes: [{tool, args}]), without committing, returns loudness, brightness and a comparison, and plays it once in the user's window. Use it when choosing between sounds (tables, presets, effect amounts), say how it sounds, then commit with the normal tools.",
|
|
61
67
|
"Wavetable synth: set_wavetable picks a table (built-ins offline, Strudel wt_ sets fetched once) and scans it by position wt.",
|
|
62
|
-
`Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains.`,
|
|
68
|
+
`Granular: set_granular makes a track a grain cloud (presets cloud hold sparkle swarm stutter microloop backwards dust) of a synth source (src synth:pad, the default; synth:<name>@<note>) or one of its own sampler voices, and overrides params (${Object.keys(GRANULAR_PARAMS).join(" ")}; repeat and hold latch the head for beat repeats, begin and end pick a region, root takes a MIDI number or note name); off returns to the previous voice; the same seed gives the same grains. Grain play: sync "1/16" puts grain onsets on the tempo grid, quant scale|chord snaps grain pitches, mono true glides legato notes through one cloud, pedal true freezes the head while the sustain pedal is down; set_automation grain-<param> lanes (grain-pos, grain-scan, grain-pitch …) move a parameter over beats. Resample: the resample tool renders a track, orbit or the mix (a section or bars) to a pinned WAV and adds a sampler track, or with grain a granular track, that plays it; set_sample shift (semitones, length kept, formant 0 keeps a voice's character) and fadeInTime/fadeTime shape a sampler voice.`,
|
|
63
69
|
"Mastering: set_master sets the song master and loudness target; measure_mix reports LUFS, true peak, balance and correlation. Master only when asked for loudness or a finished sound, and measure before and after.",
|
|
64
70
|
'Reply with exactly one JSON object and nothing else, shaped {"ops":[{"tool":"<tool name>","args":{...}}],"say":"<one short sentence describing the musical change>","done":true}.',
|
|
65
71
|
'Set "done":false only if you need to see the results of these ops before continuing; you will then get each op\'s result and can send more ops.',
|
package/src/audio/arrange.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
type RenderedAudio,
|
|
52
52
|
type WavCue,
|
|
53
53
|
} from "./wav.ts";
|
|
54
|
+
import { engineFor } from "./instruments.ts";
|
|
54
55
|
|
|
55
56
|
/** What one default render pass holds before the renderer cuts it. */
|
|
56
57
|
const DEFAULT_PASS_SECONDS = 30;
|
|
@@ -393,11 +394,31 @@ function renderWindows(
|
|
|
393
394
|
notes,
|
|
394
395
|
master: null,
|
|
395
396
|
});
|
|
397
|
+
// State an engine cannot rebuild in the pre-roll (organ rotors along a
|
|
398
|
+
// lane) is integrated over the song before the window.
|
|
399
|
+
const seedState: Record<string, string> = {};
|
|
400
|
+
if (
|
|
401
|
+
pre > 0 &&
|
|
402
|
+
window.tracks.some((track) => engineFor(track)?.windowSeed)
|
|
403
|
+
) {
|
|
404
|
+
const history = frame(0, pre);
|
|
405
|
+
for (const track of history.tracks) {
|
|
406
|
+
const seed = engineFor(track)?.windowSeed?.(
|
|
407
|
+
history,
|
|
408
|
+
track,
|
|
409
|
+
frameAt(pre),
|
|
410
|
+
sampleRate,
|
|
411
|
+
);
|
|
412
|
+
if (seed !== undefined) seedState[track.id] = seed;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
396
415
|
const audio = renderer.render(window, {
|
|
397
416
|
...options,
|
|
398
417
|
sampleRate,
|
|
399
418
|
loop: false,
|
|
400
419
|
seedTick: preTick,
|
|
420
|
+
...(pre > 0 ? { seedSeconds: secondsAtTick(timeline, pre * ticks) } : {}),
|
|
421
|
+
...(Object.keys(seedState).length > 0 ? { seedState } : {}),
|
|
401
422
|
// Past `end` too, so voices that fade where the buffer stops (the
|
|
402
423
|
// legacy tone voice) fade in frames this window drops.
|
|
403
424
|
maxSeconds: last
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cepstral spectral envelope (0.6 B2): the smooth outline of a magnitude
|
|
3
|
+
* spectrum, used by `shift.ts` to hold formants still while the pitch moves.
|
|
4
|
+
* Log magnitude, inverse transform to the real cepstrum, keep the
|
|
5
|
+
* quefrencies below the lifter, forward transform, exponentiate
|
|
6
|
+
* (Oppenheim and Schafer, *Discrete-Time Signal Processing*, ch. 13;
|
|
7
|
+
* J. O. Smith, *Spectral Audio Signal Processing*, "Cepstral Windowing";
|
|
8
|
+
* Röbel and Rodet, "Efficient spectral envelope estimation and its
|
|
9
|
+
* application to pitch shifting and envelope preservation", DAFx 2005, for
|
|
10
|
+
* the true-envelope iteration).
|
|
11
|
+
*/
|
|
12
|
+
import { fftInPlace } from "./fft.ts";
|
|
13
|
+
import { inverse } from "./stft.ts";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default lifter: quefrencies below 2 ms (measured on a synthetic vowel:
|
|
17
|
+
* with 16 true-envelope passes F1 stays within 3% after +-7 and +-12 st at
|
|
18
|
+
* 110 Hz; 1 ms smeared F1 by up to 15%).
|
|
19
|
+
*/
|
|
20
|
+
export const ENVELOPE_LIFTER_SECONDS = 0.002;
|
|
21
|
+
/** True-envelope passes used by `pitchShift` when keeping formants. */
|
|
22
|
+
export const ENVELOPE_ITERATIONS = 16;
|
|
23
|
+
|
|
24
|
+
/** Adaptive lifter as a fraction of the cepstral period. */
|
|
25
|
+
export const PERIOD_FRACTION = 0.25;
|
|
26
|
+
|
|
27
|
+
/** Scratch buffers for one frame size, reused across frames. */
|
|
28
|
+
export type EnvelopeScratch = Readonly<{
|
|
29
|
+
re: Float64Array;
|
|
30
|
+
im: Float64Array;
|
|
31
|
+
log: Float64Array;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export function envelopeScratch(n: number): EnvelopeScratch {
|
|
35
|
+
return Object.freeze({
|
|
36
|
+
re: new Float64Array(n),
|
|
37
|
+
im: new Float64Array(n),
|
|
38
|
+
log: new Float64Array(n / 2 + 1),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Writes the cepstral envelope of `mag` (bins 0..n/2 of an n-point
|
|
44
|
+
* spectrum) into `out` (length n/2 + 1). `lifter` is in samples (at least
|
|
45
|
+
* 4). With `iterations` > 0 it is the true envelope (Röbel and Rodet 2005):
|
|
46
|
+
* the log spectrum is raised to the smoothed curve wherever it lies below
|
|
47
|
+
* it and smoothed again, so the envelope rides the harmonic peaks instead
|
|
48
|
+
* of the valleys between them. With `periodRange` (samples) the lifter
|
|
49
|
+
* shrinks to a quarter of the frame's cepstral pitch period when that is
|
|
50
|
+
* shorter, so higher voices keep their harmonics out of the envelope.
|
|
51
|
+
*/
|
|
52
|
+
export function cepstralEnvelope(
|
|
53
|
+
mag: Float64Array,
|
|
54
|
+
n: number,
|
|
55
|
+
lifter: number,
|
|
56
|
+
out: Float64Array,
|
|
57
|
+
scratch: EnvelopeScratch = envelopeScratch(n),
|
|
58
|
+
iterations = 0,
|
|
59
|
+
periodRange?: readonly [number, number],
|
|
60
|
+
): Float64Array {
|
|
61
|
+
const half = n / 2;
|
|
62
|
+
const { re, im, log } = scratch;
|
|
63
|
+
let peak = 0;
|
|
64
|
+
for (let k = 0; k <= half; k += 1) peak = Math.max(peak, mag[k]!);
|
|
65
|
+
// Floor 120 dB under the frame's peak (silence stays finite).
|
|
66
|
+
const floor = peak * 1e-6 + 1e-12;
|
|
67
|
+
for (let k = 0; k <= half; k += 1) log[k] = Math.log(mag[k]! + floor);
|
|
68
|
+
let cut = Math.max(4, Math.min(half, Math.round(lifter)));
|
|
69
|
+
for (let pass = 0; pass <= iterations; pass += 1) {
|
|
70
|
+
for (let k = 0; k < n; k += 1) {
|
|
71
|
+
re[k] = log[k <= half ? k : n - k]!;
|
|
72
|
+
im[k] = 0;
|
|
73
|
+
}
|
|
74
|
+
inverse(re, im);
|
|
75
|
+
if (pass === 0 && periodRange) {
|
|
76
|
+
// Pitch-adaptive lifter: a quarter of the period found as the
|
|
77
|
+
// strongest cepstral peak in range, never longer than `lifter`.
|
|
78
|
+
const [lo, hi] = periodRange;
|
|
79
|
+
let q = Math.max(2, Math.round(lo));
|
|
80
|
+
for (let i = q; i <= Math.min(half, Math.round(hi)); i += 1)
|
|
81
|
+
if (re[i]! > re[q]!) q = i;
|
|
82
|
+
cut = Math.max(4, Math.min(cut, Math.round(q * PERIOD_FRACTION)));
|
|
83
|
+
}
|
|
84
|
+
for (let q = cut; q <= n - cut; q += 1) {
|
|
85
|
+
re[q] = 0;
|
|
86
|
+
im[q] = 0;
|
|
87
|
+
}
|
|
88
|
+
fftInPlace(re, im);
|
|
89
|
+
if (pass < iterations)
|
|
90
|
+
for (let k = 0; k <= half; k += 1) log[k] = Math.max(log[k]!, re[k]!);
|
|
91
|
+
}
|
|
92
|
+
for (let k = 0; k <= half; k += 1) out[k] = Math.exp(re[k]!);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Harmonic-peak envelope (0.6.1): log magnitude interpolated linearly
|
|
98
|
+
* between the spectral peaks (each refined by a parabola to its true bin
|
|
99
|
+
* and height) of `mag` that stand within `range` of the
|
|
100
|
+
* frame's loudest, held flat past the first and last. Between harmonics it
|
|
101
|
+
* follows the line joining them instead of the smoothed valley, so it stays
|
|
102
|
+
* on the formant at any pitch where the cepstral envelope (whose lifter
|
|
103
|
+
* must shrink with the period) starts to smear or ripple. `smooth` (an
|
|
104
|
+
* envelope from `cepstralEnvelope`) is used where no peak stands.
|
|
105
|
+
* Measured with `formant 0` on a synthetic vowel: F1 within 1.1% of the
|
|
106
|
+
* same vowel sung at the new pitch for f0 110-330 Hz at +-7 and +12 st
|
|
107
|
+
* (the cepstral envelope alone: -29% to +46% above 200 Hz).
|
|
108
|
+
*/
|
|
109
|
+
export function peakEnvelope(
|
|
110
|
+
mag: Float64Array,
|
|
111
|
+
half: number,
|
|
112
|
+
out: Float64Array,
|
|
113
|
+
smooth: Float64Array,
|
|
114
|
+
peaks: number[],
|
|
115
|
+
range = 1e-4,
|
|
116
|
+
): Float64Array {
|
|
117
|
+
let top = 0;
|
|
118
|
+
for (let k = 0; k <= half; k += 1) top = Math.max(top, mag[k]!);
|
|
119
|
+
peaks.length = 0;
|
|
120
|
+
const floor = top * range;
|
|
121
|
+
for (let k = 2; k <= half - 2; k += 1) {
|
|
122
|
+
const v = mag[k]!;
|
|
123
|
+
if (
|
|
124
|
+
v > floor &&
|
|
125
|
+
v > mag[k - 1]! &&
|
|
126
|
+
v >= mag[k + 1]! &&
|
|
127
|
+
v > mag[k - 2]! &&
|
|
128
|
+
v >= mag[k + 2]!
|
|
129
|
+
)
|
|
130
|
+
peaks.push(k);
|
|
131
|
+
}
|
|
132
|
+
if (peaks.length < 2) {
|
|
133
|
+
out.set(smooth.subarray(0, half + 1));
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
// Each peak refined by a parabola on log magnitude: its true frequency
|
|
137
|
+
// (fractional bin) and height.
|
|
138
|
+
const at = (i: number) => {
|
|
139
|
+
const k = peaks[i]!;
|
|
140
|
+
const a = Math.log(mag[k - 1]! + 1e-30);
|
|
141
|
+
const b = Math.log(mag[k]! + 1e-30);
|
|
142
|
+
const c = Math.log(mag[k + 1]! + 1e-30);
|
|
143
|
+
const d = a - 2 * b + c;
|
|
144
|
+
const delta =
|
|
145
|
+
d < 0 ? Math.max(-0.5, Math.min(0.5, (0.5 * (a - c)) / d)) : 0;
|
|
146
|
+
return [k + delta, b - 0.25 * (a - c) * delta] as const;
|
|
147
|
+
};
|
|
148
|
+
let j = 0;
|
|
149
|
+
let [x0, y0] = at(0);
|
|
150
|
+
let [x1, y1] = at(1);
|
|
151
|
+
for (let k = 0; k <= half; k += 1) {
|
|
152
|
+
while (j + 2 < peaks.length && x1 <= k) {
|
|
153
|
+
j += 1;
|
|
154
|
+
[x0, y0] = [x1, y1];
|
|
155
|
+
[x1, y1] = at(j + 1);
|
|
156
|
+
}
|
|
157
|
+
const t = Math.max(0, Math.min(1, (k - x0) / (x1 - x0)));
|
|
158
|
+
out[k] = Math.exp(y0 + (y1 - y0) * t);
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|