@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
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One blown voice: a digital waveguide bore driven by a reed table
|
|
3
|
+
* (clarinet, `reed`), a conical reed (`sax`, also oboe and bassoon), an
|
|
4
|
+
* air jet (`jet`, flutes; open or stopped) or a lip resonator (`lips`,
|
|
5
|
+
* brass). After Cook and Scavone's STK (Clarinet, Saxofony, Flute, Brass;
|
|
6
|
+
* https://github.com/thestk/stk) and J. O. Smith, Physical Audio Signal
|
|
7
|
+
* Processing (https://ccrma.stanford.edu/~jos/pasp/), ported from the
|
|
8
|
+
* reviewed prototype (.plans/dawg-06/proto/resonators/winds.ts) with the
|
|
9
|
+
* review's fixes:
|
|
10
|
+
*
|
|
11
|
+
* - jet: a two-period loop (bore 1.5 periods, jet 0.5) less the loop
|
|
12
|
+
* low-pass and DC-blocker phase delays, with a jet offset and an edge
|
|
13
|
+
* tap so open pipes get even harmonics; stopped pipes are an inverting
|
|
14
|
+
* half-period bore (odd harmonics).
|
|
15
|
+
* - jet, reed and sax run at twice the rate with first-order ADAA on their
|
|
16
|
+
* nonlinearity and the 47-tap half-band decimator; brass runs at 1x.
|
|
17
|
+
* - Thiran fractional delays, loop cutoff and the trim are retuned every
|
|
18
|
+
* 32-sample control tick (bends, glides, slurs, vibrato).
|
|
19
|
+
* - The oscillation pressure keeps a 0.45 floor for stable pitch; a
|
|
20
|
+
* separate breath gain (30 dB over breath 0..1) makes swells audible.
|
|
21
|
+
*
|
|
22
|
+
* Deterministic: noise from `seededRandom` keyed by the note.
|
|
23
|
+
*/
|
|
24
|
+
import type { WindSettings } from "../../../core/resonators.ts";
|
|
25
|
+
import { Decimate2 } from "../dsp/oversample.ts";
|
|
26
|
+
import { seededRandom } from "../dsp/rng.ts";
|
|
27
|
+
import { DcBlock, OnePole } from "../dsp/filters.ts";
|
|
28
|
+
import {
|
|
29
|
+
WindBiquad,
|
|
30
|
+
LoopDelay,
|
|
31
|
+
dcBlockDelay,
|
|
32
|
+
onePoleDelay,
|
|
33
|
+
} from "./filters.ts";
|
|
34
|
+
|
|
35
|
+
/** Control tick in base-rate samples. */
|
|
36
|
+
export const WIND_BLOCK = 32;
|
|
37
|
+
|
|
38
|
+
/** Pitch target over the voice: the first entry at 0, slurs after it. */
|
|
39
|
+
export type WindSegment = Readonly<{ at: number; hz: number }>;
|
|
40
|
+
|
|
41
|
+
export type WindNote = Readonly<{
|
|
42
|
+
/** Pitches in base-rate samples from the voice start; [0] at 0. */
|
|
43
|
+
segments: readonly WindSegment[];
|
|
44
|
+
/** Breath duration (note-off) in base-rate samples. */
|
|
45
|
+
length: number;
|
|
46
|
+
velocity: number;
|
|
47
|
+
seed: string;
|
|
48
|
+
/** Extra breath for accent/marcato (+), less for ghost (-). */
|
|
49
|
+
push?: number;
|
|
50
|
+
/** Pitch offset in cents at t seconds (bends, glides). */
|
|
51
|
+
cents?: (t: number) => number;
|
|
52
|
+
/** Automation at t seconds; returns undefined for a parameter with no lane. */
|
|
53
|
+
lane?: (param: string, t: number) => number | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
|
|
56
|
+
/** Per-voice trims: cents as a function of sounding Hz, output gain. */
|
|
57
|
+
export type WindTrim = Readonly<{
|
|
58
|
+
cents: (hz: number) => number;
|
|
59
|
+
level: number;
|
|
60
|
+
/** Loudness curve over the range: a gain factor at `hz` (default 1). */
|
|
61
|
+
gain?: (hz: number) => number;
|
|
62
|
+
/**
|
|
63
|
+
* Calibration 1 lips (q08): the lip resonance sits on the sounding pitch
|
|
64
|
+
* at Q 40 and the lip opening saturates softly, so the lips lock to the
|
|
65
|
+
* bore's mode instead of beating against it (a 23-35% warble every 8 or
|
|
66
|
+
* 9 periods before). Only the calibration 1 trim rows set it.
|
|
67
|
+
*/
|
|
68
|
+
lips?: 1;
|
|
69
|
+
}>;
|
|
70
|
+
|
|
71
|
+
const NO_TRIM: WindTrim = { cents: () => 0, level: 1 };
|
|
72
|
+
|
|
73
|
+
const SLUR_SECONDS = 0.03;
|
|
74
|
+
/** First-order ADAA delays its shaper's output by half a sample. */
|
|
75
|
+
const ADAA_DELAY = 0.5;
|
|
76
|
+
const clamp = (x: number, lo: number, hi: number) =>
|
|
77
|
+
x < lo ? lo : x > hi ? hi : x;
|
|
78
|
+
|
|
79
|
+
// Reed table g(p) = p * clamp(0.7 - s p, -1, 1) and its antiderivative (ADAA).
|
|
80
|
+
function reedG(p: number, s: number): number {
|
|
81
|
+
return p * clamp(0.7 - s * p, -1, 1);
|
|
82
|
+
}
|
|
83
|
+
function reedF(p: number, s: number): number {
|
|
84
|
+
const p1 = -0.3 / s;
|
|
85
|
+
const p2 = 1.7 / s;
|
|
86
|
+
const mid = (x: number) => 0.35 * x * x - (s * x * x * x) / 3;
|
|
87
|
+
if (p < p1) return 0.5 * p * p + mid(p1) - 0.5 * p1 * p1;
|
|
88
|
+
if (p > p2) return -0.5 * p * p + mid(p2) + 0.5 * p2 * p2;
|
|
89
|
+
return mid(p);
|
|
90
|
+
}
|
|
91
|
+
// Jet table j(x) = clamp(x^3 - x, -1, 1) and its antiderivative.
|
|
92
|
+
const JET_EDGE = 1.3247179572447458; // real root of x^3 - x - 1
|
|
93
|
+
function jetG(x: number): number {
|
|
94
|
+
return clamp(x * x * x - x, -1, 1);
|
|
95
|
+
}
|
|
96
|
+
function jetF(x: number): number {
|
|
97
|
+
const a = Math.abs(x);
|
|
98
|
+
const poly = (v: number) => 0.25 * v ** 4 - 0.5 * v * v;
|
|
99
|
+
if (a <= JET_EDGE) return poly(x);
|
|
100
|
+
return poly(JET_EDGE) + (a - JET_EDGE);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* STK's lip filter: a two-pole resonance at the lip frequency with no
|
|
105
|
+
* zeros (BiQuad::setResonance, unnormalised), so the mouth pressure's DC
|
|
106
|
+
* sets the lips' operating point and the squared area mapping has gain.
|
|
107
|
+
*/
|
|
108
|
+
class LipResonator {
|
|
109
|
+
private a1 = 0;
|
|
110
|
+
private a2 = 0;
|
|
111
|
+
private y1 = 0;
|
|
112
|
+
private y2 = 0;
|
|
113
|
+
private g = 1;
|
|
114
|
+
set(hz: number, q: number, gain: number, sampleRate: number): void {
|
|
115
|
+
const radius = Math.exp((-Math.PI * hz) / (q * sampleRate));
|
|
116
|
+
this.a1 = -2 * radius * Math.cos((2 * Math.PI * hz) / sampleRate);
|
|
117
|
+
this.a2 = radius * radius;
|
|
118
|
+
this.g = gain * (1 + this.a1 + this.a2);
|
|
119
|
+
}
|
|
120
|
+
process(x: number): number {
|
|
121
|
+
const y = this.g * x - this.a1 * this.y1 - this.a2 * this.y2;
|
|
122
|
+
this.y2 = this.y1;
|
|
123
|
+
this.y1 = y;
|
|
124
|
+
return y;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** First-order ADAA around a shaper with a parameter. */
|
|
129
|
+
class Adaa {
|
|
130
|
+
private x1 = 0;
|
|
131
|
+
private F1: number;
|
|
132
|
+
constructor(
|
|
133
|
+
private readonly f: (x: number) => number,
|
|
134
|
+
private readonly F: (x: number) => number,
|
|
135
|
+
) {
|
|
136
|
+
this.F1 = F(0);
|
|
137
|
+
}
|
|
138
|
+
process(x: number): number {
|
|
139
|
+
const Fx = this.F(x);
|
|
140
|
+
const dx = x - this.x1;
|
|
141
|
+
const y =
|
|
142
|
+
Math.abs(dx) < 1e-6 ? this.f(0.5 * (x + this.x1)) : (Fx - this.F1) / dx;
|
|
143
|
+
this.x1 = x;
|
|
144
|
+
this.F1 = Fx;
|
|
145
|
+
return y;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export class WindVoice {
|
|
150
|
+
/** Base-rate sample after which the voice is silent. */
|
|
151
|
+
readonly endAt: number;
|
|
152
|
+
private readonly rate: number;
|
|
153
|
+
private readonly os: boolean;
|
|
154
|
+
private readonly random: () => number;
|
|
155
|
+
private readonly d0: LoopDelay;
|
|
156
|
+
private readonly d1: LoopDelay;
|
|
157
|
+
private readonly loopLp: OnePole;
|
|
158
|
+
private readonly dc: DcBlock;
|
|
159
|
+
private readonly outDc: DcBlock;
|
|
160
|
+
private readonly noiseLp: OnePole;
|
|
161
|
+
private readonly lip = new LipResonator();
|
|
162
|
+
private readonly shelf = new WindBiquad();
|
|
163
|
+
/** Reed, sax and lips brightness: a pitch-tracked tilt outside the loop. */
|
|
164
|
+
private readonly tone = new WindBiquad();
|
|
165
|
+
private readonly mutes: WindBiquad[] = [];
|
|
166
|
+
private readonly wahFilter = new WindBiquad();
|
|
167
|
+
private readonly decimate: Decimate2 | undefined;
|
|
168
|
+
private readonly shaper: Adaa | undefined;
|
|
169
|
+
private readonly reedSlope: number;
|
|
170
|
+
private settings: WindSettings;
|
|
171
|
+
private n = 0;
|
|
172
|
+
private envelope = 0;
|
|
173
|
+
private oz1 = 0;
|
|
174
|
+
private hz: number;
|
|
175
|
+
private slurFrom = 0;
|
|
176
|
+
private slurAt = -1;
|
|
177
|
+
private segment = 0;
|
|
178
|
+
private pressure = 0;
|
|
179
|
+
private breathGain = 1;
|
|
180
|
+
private wah = 1;
|
|
181
|
+
/** Output gain the loudness curve asks for, and its smoothed value. */
|
|
182
|
+
private gainTarget = 1;
|
|
183
|
+
private gainNow = -1;
|
|
184
|
+
private readonly growlHz: number;
|
|
185
|
+
|
|
186
|
+
constructor(
|
|
187
|
+
base: WindSettings,
|
|
188
|
+
private readonly note: WindNote,
|
|
189
|
+
private readonly sampleRate: number,
|
|
190
|
+
private readonly trim: WindTrim = NO_TRIM,
|
|
191
|
+
) {
|
|
192
|
+
this.settings = base;
|
|
193
|
+
const p = base;
|
|
194
|
+
this.os = p.model !== "lips";
|
|
195
|
+
this.rate = this.os ? 2 * sampleRate : sampleRate;
|
|
196
|
+
this.random = seededRandom(`${note.seed}:wind`);
|
|
197
|
+
let lowest = Infinity;
|
|
198
|
+
for (const s of note.segments) lowest = Math.min(lowest, s.hz);
|
|
199
|
+
lowest = Math.max(15, lowest) * 0.5; // room for a -1 octave bend
|
|
200
|
+
const maxDelay = (this.rate / lowest) * 2.2 + 16;
|
|
201
|
+
this.d0 = new LoopDelay(maxDelay);
|
|
202
|
+
this.d1 = new LoopDelay(maxDelay);
|
|
203
|
+
this.loopLp = new OnePole(4000, this.rate);
|
|
204
|
+
this.dc = new DcBlock(this.rate, 20);
|
|
205
|
+
this.outDc = new DcBlock(sampleRate, 20);
|
|
206
|
+
this.noiseLp = new OnePole(2500, this.rate);
|
|
207
|
+
this.decimate = this.os ? new Decimate2() : undefined;
|
|
208
|
+
this.reedSlope = 0.3 * (0.6 + 0.8 * p.reed);
|
|
209
|
+
const s = this.reedSlope;
|
|
210
|
+
if (p.model === "reed")
|
|
211
|
+
this.shaper = new Adaa(
|
|
212
|
+
(x) => reedG(x, s),
|
|
213
|
+
(x) => reedF(x, s),
|
|
214
|
+
);
|
|
215
|
+
else if (p.model === "jet") this.shaper = new Adaa(jetG, jetF);
|
|
216
|
+
this.hz = note.segments[0]!.hz;
|
|
217
|
+
this.growlHz = 70 + 50 * this.random();
|
|
218
|
+
// Dynamics: a 1.5 kHz shelf of (v - 0.6) k dB; brass blares, flutes barely change.
|
|
219
|
+
const k = { jet: 8, reed: 10, sax: 14, lips: 22 }[p.model];
|
|
220
|
+
const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
|
|
221
|
+
this.shelf.set("highshelf", 1500, 0.7, (v - 0.6) * k, sampleRate);
|
|
222
|
+
this.setupMute(sampleRate);
|
|
223
|
+
this.endAt = note.length + Math.round((p.release * 3 + 0.05) * sampleRate);
|
|
224
|
+
this.control(0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private setupMute(sr: number): void {
|
|
228
|
+
const add = (
|
|
229
|
+
type: Parameters<WindBiquad["set"]>[0],
|
|
230
|
+
hz: number,
|
|
231
|
+
q: number,
|
|
232
|
+
db: number,
|
|
233
|
+
) => this.mutes.push(new WindBiquad().set(type, hz, q, db, sr));
|
|
234
|
+
switch (this.settings.mute) {
|
|
235
|
+
case "straight":
|
|
236
|
+
add("hpf", 450, 0.7, 0);
|
|
237
|
+
add("peak", 1800, 2, 9);
|
|
238
|
+
add("lpf", 7000, 0.7, 0);
|
|
239
|
+
break;
|
|
240
|
+
case "cup":
|
|
241
|
+
add("hpf", 220, 0.7, 0);
|
|
242
|
+
add("peak", 850, 1.5, 5);
|
|
243
|
+
add("lpf", 2200, 0.8, 0);
|
|
244
|
+
break;
|
|
245
|
+
case "harmon":
|
|
246
|
+
add("hpf", 700, 0.8, 0);
|
|
247
|
+
add("peak", 1650, 4, 14);
|
|
248
|
+
add("lpf", 4200, 1, 0);
|
|
249
|
+
break;
|
|
250
|
+
default:
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Control tick: lanes, pitch (slur, cents, vibrato, trim), delays, breath. */
|
|
256
|
+
private control(n: number): void {
|
|
257
|
+
const { note, sampleRate: sr, rate } = this;
|
|
258
|
+
const t = n / sr;
|
|
259
|
+
let p = this.settings;
|
|
260
|
+
if (note.lane) {
|
|
261
|
+
const next: Record<string, unknown> = { ...p };
|
|
262
|
+
let changed = false;
|
|
263
|
+
for (const key of ["breath", "noise", "wah", "growl", "flutter"]) {
|
|
264
|
+
const value = note.lane(key, t);
|
|
265
|
+
if (value !== undefined) {
|
|
266
|
+
next[key] = clamp(value, 0, 1);
|
|
267
|
+
changed = true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (changed) p = this.settings = next as WindSettings;
|
|
271
|
+
}
|
|
272
|
+
// Slurs: a short log-frequency glide into each later segment.
|
|
273
|
+
while (
|
|
274
|
+
this.segment + 1 < note.segments.length &&
|
|
275
|
+
note.segments[this.segment + 1]!.at <= n
|
|
276
|
+
) {
|
|
277
|
+
this.segment += 1;
|
|
278
|
+
this.slurFrom = this.hz;
|
|
279
|
+
this.slurAt = note.segments[this.segment]!.at;
|
|
280
|
+
}
|
|
281
|
+
const target = note.segments[this.segment]!.hz;
|
|
282
|
+
let hz = target;
|
|
283
|
+
if (this.slurAt >= 0) {
|
|
284
|
+
const u = (n - this.slurAt) / (SLUR_SECONDS * sr);
|
|
285
|
+
if (u >= 1) this.slurAt = -1;
|
|
286
|
+
else hz = this.slurFrom * (target / this.slurFrom) ** u;
|
|
287
|
+
}
|
|
288
|
+
this.hz = hz;
|
|
289
|
+
this.gainTarget = this.trim.gain ? this.trim.gain(hz) : 1;
|
|
290
|
+
if (this.gainNow < 0) this.gainNow = this.gainTarget;
|
|
291
|
+
const cents = note.cents ? note.cents(t) : 0;
|
|
292
|
+
const vib =
|
|
293
|
+
p.vibmod > 0
|
|
294
|
+
? p.vibmod * Math.sin(2 * Math.PI * p.vib * t) * Math.min(1, t / 0.3)
|
|
295
|
+
: 0;
|
|
296
|
+
const f0 = hz * 2 ** (cents / 1200 + vib / 12);
|
|
297
|
+
const f = f0 * 2 ** (this.trim.cents(f0) / 1200);
|
|
298
|
+
const w = (2 * Math.PI * f) / rate;
|
|
299
|
+
const period = rate / f;
|
|
300
|
+
switch (p.model) {
|
|
301
|
+
case "reed":
|
|
302
|
+
// Closed-open cylinder: inverting reflection, round trip = 2 x loop;
|
|
303
|
+
// the one-zero averager adds half a sample.
|
|
304
|
+
this.d0.set(period / 2 - 0.5 - ADAA_DELAY, w);
|
|
305
|
+
break;
|
|
306
|
+
case "sax": {
|
|
307
|
+
// Reed position 0.35 along the bore: measured over every semitone of
|
|
308
|
+
// sax, altosax, barisax, oboe and bassoon at 22.05, 44.1 and 48 kHz
|
|
309
|
+
// it never flips register (STK's 0.2 flips at 6 of 36 sax notes,
|
|
310
|
+
// the spec's register guard).
|
|
311
|
+
const position = 0.35;
|
|
312
|
+
const total = period - 0.5;
|
|
313
|
+
this.d0.set((1 - position) * total, w);
|
|
314
|
+
this.d1.set(position * total, w);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "jet": {
|
|
318
|
+
// Loop low-pass corner tracks the pitch: brighter above.
|
|
319
|
+
const corner = clamp(f * (3 + 9 * p.bright), 800, 0.4 * rate);
|
|
320
|
+
this.loopLp.set(corner, rate);
|
|
321
|
+
const filters =
|
|
322
|
+
onePoleDelay(this.loopLp.p, w) +
|
|
323
|
+
dcBlockDelay(this.dc.r, w) +
|
|
324
|
+
ADAA_DELAY;
|
|
325
|
+
if (p.stopped) {
|
|
326
|
+
this.d0.set(0.5 * period - filters, w);
|
|
327
|
+
this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
|
|
328
|
+
} else {
|
|
329
|
+
this.d0.set(1.5 * period - filters, w);
|
|
330
|
+
this.d1.set(0.5 * period * (0.9 + 0.2 * p.reed), w);
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
case "lips":
|
|
335
|
+
// STK Brass: a bore of two periods sounding its 2nd mode and a lip
|
|
336
|
+
// resonance a little below f (constant Q 12, unit DC gain x 3 so
|
|
337
|
+
// every register speaks); the trim removes the pull (+15..45 c).
|
|
338
|
+
// Calibration 1 tunes the lips to f at Q 40 (see WindTrim.lips).
|
|
339
|
+
this.d0.set(2 * period - dcBlockDelay(this.dc.r, w), w);
|
|
340
|
+
if (this.trim.lips)
|
|
341
|
+
this.lip.set(f * (1 + 0.04 * (p.reed - 0.5)), 40, 3, rate);
|
|
342
|
+
else this.lip.set(f * (0.95 + 0.04 * (p.reed - 0.5)), 12, 3, rate);
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
// Brightness for the reed, sax and lip bores: a high shelf from about
|
|
346
|
+
// the 4th harmonic, +-9 dB around bright 0.5. It sits outside the loop
|
|
347
|
+
// (after the bore, before the mute), so pitch and the trims stay put;
|
|
348
|
+
// the jet carries bright in its loop low-pass instead.
|
|
349
|
+
if (p.model !== "jet")
|
|
350
|
+
this.tone.set(
|
|
351
|
+
"highshelf",
|
|
352
|
+
clamp(4 * hz, 500, 0.4 * sr),
|
|
353
|
+
0.7,
|
|
354
|
+
(p.bright - 0.5) * 18,
|
|
355
|
+
sr,
|
|
356
|
+
);
|
|
357
|
+
// Oscillation pressure keeps a floor; breath gain follows breath.
|
|
358
|
+
const v = clamp(note.velocity + (note.push ?? 0), 0, 1);
|
|
359
|
+
const drive = clamp(p.breath * (0.55 + 0.45 * v), 0, 1);
|
|
360
|
+
const floor = 0.45;
|
|
361
|
+
switch (p.model) {
|
|
362
|
+
case "reed":
|
|
363
|
+
this.pressure = floor + 0.45 * drive;
|
|
364
|
+
break;
|
|
365
|
+
case "sax":
|
|
366
|
+
this.pressure = floor + 0.45 * drive;
|
|
367
|
+
break;
|
|
368
|
+
case "jet":
|
|
369
|
+
this.pressure = 0.9 + 0.35 * drive;
|
|
370
|
+
break;
|
|
371
|
+
case "lips":
|
|
372
|
+
this.pressure = 0.6 + 0.5 * drive;
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
this.breathGain = 10 ** ((30 * (p.breath - 0.6)) / 20);
|
|
376
|
+
// Plunger: wah, opened per note by wahenv.
|
|
377
|
+
if (p.mute === "plunger") {
|
|
378
|
+
const open = Math.min(1, t / 0.15);
|
|
379
|
+
this.wah = p.wah + p.wahenv * (1 - p.wah) * open;
|
|
380
|
+
this.wahFilter.set("lpf", 400 + 4600 * this.wah * this.wah, 1.4, 0, sr);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** One internal-rate sample of the bore. */
|
|
385
|
+
private tick(breath: number): number {
|
|
386
|
+
const p = this.settings;
|
|
387
|
+
switch (p.model) {
|
|
388
|
+
case "reed": {
|
|
389
|
+
const bore = this.d0.read();
|
|
390
|
+
const oz = 0.5 * (bore + this.oz1);
|
|
391
|
+
this.oz1 = bore;
|
|
392
|
+
const pdiff = -0.95 * oz - breath;
|
|
393
|
+
this.d0.push(breath + this.shaper!.process(pdiff));
|
|
394
|
+
return bore;
|
|
395
|
+
}
|
|
396
|
+
case "sax": {
|
|
397
|
+
const a = this.d0.read();
|
|
398
|
+
const b = this.d1.read();
|
|
399
|
+
const oz = 0.5 * (a + this.oz1);
|
|
400
|
+
this.oz1 = a;
|
|
401
|
+
const temp = -0.95 * oz;
|
|
402
|
+
const frame = temp - b;
|
|
403
|
+
const pdiff = breath - frame;
|
|
404
|
+
this.d1.push(temp);
|
|
405
|
+
// Plain reed table (no ADAA): measured, ADAA's half-sample average
|
|
406
|
+
// destabilises the conical loop above ~300 Hz; 2x keeps aliasing low.
|
|
407
|
+
this.d0.push(
|
|
408
|
+
breath - pdiff * clamp(0.7 + this.reedSlope * pdiff, -1, 1) - temp,
|
|
409
|
+
);
|
|
410
|
+
return frame;
|
|
411
|
+
}
|
|
412
|
+
case "jet": {
|
|
413
|
+
const boreOut = this.d0.read();
|
|
414
|
+
const temp = this.dc.process(-this.loopLp.process(boreOut));
|
|
415
|
+
const jet = this.d1.read();
|
|
416
|
+
this.d1.push(breath - 0.5 * temp);
|
|
417
|
+
// Jet offset: the jet hits the edge off-centre, giving even harmonics.
|
|
418
|
+
const offset = p.stopped ? 0 : 0.12;
|
|
419
|
+
const jt = this.shaper!.process(jet + offset) - jetG(offset);
|
|
420
|
+
this.d0.push(jt + 0.5 * temp);
|
|
421
|
+
// Edge tap (open pipes): some of the jet itself is heard.
|
|
422
|
+
return 0.3 * boreOut + (p.stopped ? 0 : 0.12 * jt);
|
|
423
|
+
}
|
|
424
|
+
case "lips": {
|
|
425
|
+
const bore = 0.85 * this.d0.read();
|
|
426
|
+
const mouth = 0.3 * breath;
|
|
427
|
+
let delta = this.lip.process(mouth - bore);
|
|
428
|
+
delta *= delta;
|
|
429
|
+
// Calibration 1: a soft opening (x / (1 + x)) in place of the hard
|
|
430
|
+
// clamp, whose corner fed the warble.
|
|
431
|
+
if (this.trim.lips) delta = delta / (1 + delta);
|
|
432
|
+
else if (delta > 1) delta = 1;
|
|
433
|
+
const frame = delta * mouth + (1 - delta) * bore;
|
|
434
|
+
this.d0.push(this.dc.process(frame));
|
|
435
|
+
return bore * 2;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Adds `count` base-rate samples into `out` from `offset`; false once the
|
|
442
|
+
* voice has finished.
|
|
443
|
+
*/
|
|
444
|
+
process(out: Float64Array, offset: number, count: number): boolean {
|
|
445
|
+
const { sampleRate: sr, note } = this;
|
|
446
|
+
let p = this.settings;
|
|
447
|
+
const attack = Math.max(1, p.attack * sr);
|
|
448
|
+
const release = Math.max(1, p.release * sr);
|
|
449
|
+
const level = this.trim.level * p.gain;
|
|
450
|
+
for (let i = 0; i < count; i += 1) {
|
|
451
|
+
const n = this.n;
|
|
452
|
+
if (n >= this.endAt) return false;
|
|
453
|
+
if (n % WIND_BLOCK === 0 && n > 0) {
|
|
454
|
+
this.control(n);
|
|
455
|
+
p = this.settings;
|
|
456
|
+
}
|
|
457
|
+
if (n < note.length)
|
|
458
|
+
this.envelope = Math.min(1, this.envelope + 1 / attack);
|
|
459
|
+
else this.envelope = Math.max(0, this.envelope - 1 / release);
|
|
460
|
+
const t = n / sr;
|
|
461
|
+
let amp = this.pressure * this.envelope;
|
|
462
|
+
if (p.vibmod > 0) amp *= 1 + 0.04 * Math.sin(2 * Math.PI * p.vib * t);
|
|
463
|
+
if (p.flutter > 0)
|
|
464
|
+
amp *=
|
|
465
|
+
1 - p.flutter * 0.5 * (0.5 + 0.5 * Math.sin(2 * Math.PI * 26 * t));
|
|
466
|
+
if (p.growl > 0)
|
|
467
|
+
amp *= 1 + p.growl * 0.25 * Math.sin(2 * Math.PI * this.growlHz * t);
|
|
468
|
+
let y: number;
|
|
469
|
+
if (this.os) {
|
|
470
|
+
const a = this.tick(this.breath(amp, p));
|
|
471
|
+
const b = this.tick(this.breath(amp, p));
|
|
472
|
+
y = this.decimate!.process(a, b);
|
|
473
|
+
} else y = this.tick(this.breath(amp, p));
|
|
474
|
+
if (p.growl > 0) y = Math.tanh(y * (1 + 2 * p.growl)) / (1 + p.growl);
|
|
475
|
+
if (p.model !== "jet") y = this.tone.process(y);
|
|
476
|
+
for (const filter of this.mutes) y = filter.process(y);
|
|
477
|
+
if (p.mute === "plunger") y = this.wahFilter.process(y);
|
|
478
|
+
y = this.shelf.process(y);
|
|
479
|
+
this.gainNow += (this.gainTarget - this.gainNow) * 0.002;
|
|
480
|
+
out[offset + i]! +=
|
|
481
|
+
this.outDc.process(y) * level * this.gainNow * this.breathGain;
|
|
482
|
+
this.n += 1;
|
|
483
|
+
}
|
|
484
|
+
return true;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Breath pressure with low-passed noise. */
|
|
488
|
+
private breath(amp: number, p: WindSettings): number {
|
|
489
|
+
const noise = this.noiseLp.process(this.random() * 2 - 1);
|
|
490
|
+
return amp * (1 + p.noise * 1.6 * noise);
|
|
491
|
+
}
|
|
492
|
+
}
|
package/src/auth/cli.ts
CHANGED
|
@@ -24,19 +24,20 @@ import { selectProvider } from "../agent/provider.ts";
|
|
|
24
24
|
import { audioStatusLine } from "../audio/engine.ts";
|
|
25
25
|
|
|
26
26
|
export const AUTH_USAGE = `Usage:
|
|
27
|
-
dawg
|
|
28
|
-
dawg
|
|
29
|
-
dawg
|
|
30
|
-
dawg
|
|
31
|
-
dawg
|
|
32
|
-
dawg
|
|
33
|
-
dawg
|
|
34
|
-
dawg
|
|
35
|
-
dawg
|
|
36
|
-
dawg
|
|
27
|
+
dawg model key find existing setups and pick a provider (reuses a saved one)
|
|
28
|
+
dawg model key gateway Vercel AI Gateway: existing key, or create one with the Vercel CLI
|
|
29
|
+
dawg model key openrouter OpenRouter: authorize in your browser (OAuth PKCE)
|
|
30
|
+
dawg model key codex ChatGPT/Codex subscription through xcb
|
|
31
|
+
dawg model key claude Claude subscription through xcb
|
|
32
|
+
dawg model key --pick show the picker even when a provider is saved
|
|
33
|
+
dawg model key --key paste an AI Gateway key (hidden input)
|
|
34
|
+
dawg model key --xcb any xcb account (Devin included)
|
|
35
|
+
dawg model key gateway --budget <dollars> spend limit for a created key
|
|
36
|
+
dawg model key codex|claude [--account <id>] [--model <key>]
|
|
37
37
|
dawg model [<alias>|<vendor/model>] pick the model (cost per prompt shown)
|
|
38
38
|
dawg logout [gateway|openrouter|codex|claude|xcb] forget keys and the saved choice
|
|
39
|
-
dawg auth status [--check] every provider: detected, active, validated
|
|
39
|
+
dawg auth status [--check] every provider: detected, active, validated
|
|
40
|
+
The older spelling of \`model key\` still works as an alias.`;
|
|
40
41
|
|
|
41
42
|
/** Entry for `dawg login|logout|auth|model`. Returns the process exit code. */
|
|
42
43
|
export async function runAuthCommand(argv: readonly string[]): Promise<number> {
|
|
@@ -82,40 +83,41 @@ export async function runAuthCommand(argv: readonly string[]): Promise<number> {
|
|
|
82
83
|
return login(parsed.target, deps, parsed.options);
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/** A one-time card the TUI shows at launch about the agent. */
|
|
87
|
+
export type FirstRunCard = Readonly<{
|
|
88
|
+
text: string;
|
|
89
|
+
tone: "info" | "warning";
|
|
90
|
+
}>;
|
|
91
|
+
|
|
85
92
|
/**
|
|
86
|
-
* The
|
|
87
|
-
*
|
|
88
|
-
*
|
|
93
|
+
* The launch note about the agent. dawg opens straight into the TUI: music
|
|
94
|
+
* first, sign-in optional. The first session with no provider gets one
|
|
95
|
+
* faint card, `/model key adds an agent · optional`, and records that it was
|
|
96
|
+
* shown so later sessions stay quiet; a saved sign-in that stopped working
|
|
97
|
+
* says so. `dawg login` and `/login` keep the picker.
|
|
89
98
|
*/
|
|
90
|
-
export async function
|
|
99
|
+
export async function firstRunCard(
|
|
91
100
|
runner: CommandRunner = systemRunner,
|
|
92
|
-
): Promise<
|
|
101
|
+
): Promise<FirstRunCard | undefined> {
|
|
93
102
|
const auth = defaultAuthEnv(runner);
|
|
94
103
|
const config = await readConfig(auth).catch(() => ({}) as DawgConfig);
|
|
95
|
-
if (auth.env.DAWG_PROVIDER) return;
|
|
96
|
-
if (!config.provider && config.setup === "skipped") return;
|
|
104
|
+
if (auth.env.DAWG_PROVIDER) return undefined;
|
|
105
|
+
if (!config.provider && config.setup === "skipped") return undefined;
|
|
97
106
|
const selection = await selectProvider(auth).catch(() => undefined);
|
|
98
|
-
if (!selection || selection.kind !== "offline") return;
|
|
99
|
-
if (config.provider && !selection.invalidSaved) return;
|
|
100
|
-
const io = terminalIO();
|
|
101
|
-
if (!io.interactive) return;
|
|
107
|
+
if (!selection || selection.kind !== "offline") return undefined;
|
|
102
108
|
if (selection.invalidSaved)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
io.print(
|
|
111
|
-
`sign-in failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
-
);
|
|
113
|
-
return 1;
|
|
114
|
-
});
|
|
115
|
-
if (code !== 0 && !config.provider)
|
|
116
|
-
await writeConfig(auth, { setup: "skipped" }).catch(() => undefined);
|
|
109
|
+
return {
|
|
110
|
+
text: `saved sign-in stopped working: ${selection.reason} · /login`,
|
|
111
|
+
tone: "warning",
|
|
112
|
+
};
|
|
113
|
+
if (config.provider) return undefined;
|
|
114
|
+
await writeConfig(auth, { setup: "skipped" }).catch(() => undefined);
|
|
115
|
+
return { text: FIRST_RUN_CARD, tone: "info" };
|
|
117
116
|
}
|
|
118
117
|
|
|
118
|
+
/** The first session's agent note. */
|
|
119
|
+
export const FIRST_RUN_CARD = "/model key adds an agent · optional";
|
|
120
|
+
|
|
119
121
|
/** `/login` in the TUI, run while the screen is handed back to the shell. */
|
|
120
122
|
export async function runTuiLogin(
|
|
121
123
|
target: LoginTarget,
|
package/src/auth/credentials.ts
CHANGED
|
@@ -83,11 +83,29 @@ export type DawgConfig = Readonly<{
|
|
|
83
83
|
openrouterModel?: string;
|
|
84
84
|
/** Set when the first-run picker was dismissed, so it does not nag. */
|
|
85
85
|
setup?: "skipped";
|
|
86
|
+
/** Show-me level of agent turns (`/showme`); absent is on. */
|
|
87
|
+
showMe?: "on" | "quiet" | "off";
|
|
86
88
|
}>;
|
|
87
89
|
|
|
88
90
|
/** `vendor/model` IDs as the AI Gateway and OpenRouter spell them. */
|
|
89
91
|
export const PROVIDER_MODEL_ID_PATTERN =
|
|
90
92
|
/^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}$/i;
|
|
93
|
+
/**
|
|
94
|
+
* OpenRouter also takes a routing variant after a colon: `:nitro`
|
|
95
|
+
* (throughput first), `:floor` (price first), `:free`, `:online` and so on.
|
|
96
|
+
*/
|
|
97
|
+
export const OPENROUTER_MODEL_ID_PATTERN =
|
|
98
|
+
/^[a-z0-9][a-z0-9-]{0,63}\/[a-z0-9][a-z0-9._-]{0,127}(?::[a-z0-9-]{1,32})?$/i;
|
|
99
|
+
|
|
100
|
+
/** True for an ID the given provider accepts; any provider when omitted. */
|
|
101
|
+
export function isProviderModelId(
|
|
102
|
+
id: string,
|
|
103
|
+
provider?: "gateway" | "openrouter",
|
|
104
|
+
): boolean {
|
|
105
|
+
return provider === "gateway"
|
|
106
|
+
? PROVIDER_MODEL_ID_PATTERN.test(id)
|
|
107
|
+
: OPENROUTER_MODEL_ID_PATTERN.test(id);
|
|
108
|
+
}
|
|
91
109
|
|
|
92
110
|
export type CredentialSource = "env" | "keychain" | "file" | "oidc";
|
|
93
111
|
export type ResolvedKey = Readonly<{ key: string; source: CredentialSource }>;
|
|
@@ -314,6 +332,7 @@ export function parseConfig(value: unknown): DawgConfig {
|
|
|
314
332
|
gatewayModel?: string;
|
|
315
333
|
openrouterModel?: string;
|
|
316
334
|
setup?: "skipped";
|
|
335
|
+
showMe?: "on" | "quiet" | "off";
|
|
317
336
|
} = {};
|
|
318
337
|
if (PROVIDER_CHOICES.includes(record.provider as ProviderChoice))
|
|
319
338
|
config.provider = record.provider as ProviderChoice;
|
|
@@ -323,10 +342,19 @@ export function parseConfig(value: unknown): DawgConfig {
|
|
|
323
342
|
}
|
|
324
343
|
for (const field of ["gatewayModel", "openrouterModel"] as const) {
|
|
325
344
|
const id = record[field];
|
|
326
|
-
if (
|
|
345
|
+
if (
|
|
346
|
+
typeof id === "string" &&
|
|
347
|
+
isProviderModelId(id, field === "gatewayModel" ? "gateway" : "openrouter")
|
|
348
|
+
)
|
|
327
349
|
config[field] = id;
|
|
328
350
|
}
|
|
329
351
|
if (record.setup === "skipped") config.setup = "skipped";
|
|
352
|
+
if (
|
|
353
|
+
record.showMe === "on" ||
|
|
354
|
+
record.showMe === "quiet" ||
|
|
355
|
+
record.showMe === "off"
|
|
356
|
+
)
|
|
357
|
+
config.showMe = record.showMe;
|
|
330
358
|
return config;
|
|
331
359
|
}
|
|
332
360
|
|
|
@@ -350,6 +378,7 @@ export type ConfigPatch = Partial<{
|
|
|
350
378
|
gatewayModel: string | undefined;
|
|
351
379
|
openrouterModel: string | undefined;
|
|
352
380
|
setup: "skipped" | undefined;
|
|
381
|
+
showMe: "on" | "quiet" | "off" | undefined;
|
|
353
382
|
}>;
|
|
354
383
|
|
|
355
384
|
export async function writeConfig(
|