hayao 0.2.0 → 0.3.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/README.md +113 -24
- package/bin/create-hayao.mjs +380 -0
- package/bin/hayao-mcp-cli.mjs +11 -0
- package/dist/app/browser.d.ts +33 -2
- package/dist/app/game.d.ts +47 -2
- package/dist/app/tuning.d.ts +68 -0
- package/dist/art/palette.d.ts +41 -0
- package/dist/audio/adaptive.d.ts +58 -0
- package/dist/audio/album.d.ts +16 -0
- package/dist/audio/analysis.d.ts +59 -0
- package/dist/audio/audio.d.ts +21 -0
- package/dist/audio/chord.d.ts +17 -0
- package/dist/audio/genres.d.ts +11 -0
- package/dist/audio/lint.d.ts +29 -0
- package/dist/audio/match.d.ts +38 -0
- package/dist/audio/music.d.ts +88 -0
- package/dist/audio/pcm.d.ts +54 -0
- package/dist/audio/quality.d.ts +28 -0
- package/dist/audio/reverb.d.ts +15 -0
- package/dist/audio/synth.d.ts +56 -0
- package/dist/audio/theory.d.ts +64 -0
- package/dist/content/campaign.d.ts +69 -0
- package/dist/content/generate.d.ts +78 -0
- package/dist/content/level.d.ts +93 -0
- package/dist/content/worldgraph.d.ts +73 -0
- package/dist/core/dmath.d.ts +2 -0
- package/dist/hayao.global.js +15 -0
- package/dist/index.d.ts +30 -1
- package/dist/index.js +4293 -434
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +15 -0
- package/dist/input/source.d.ts +52 -1
- package/dist/mcp.js +31225 -0
- package/dist/rasterize-worker-lite.mjs +13 -0
- package/dist/render/canvas.d.ts +6 -1
- package/dist/render/commands.d.ts +46 -0
- package/dist/render/paint.d.ts +33 -0
- package/dist/render/renderer.d.ts +22 -0
- package/dist/render/svg.d.ts +4 -1
- package/dist/render/svgString.d.ts +6 -2
- package/dist/scene/cameraController.d.ts +42 -0
- package/dist/scene/node.d.ts +17 -4
- package/dist/scene/nodes.d.ts +14 -0
- package/dist/scene/parallax.d.ts +15 -0
- package/dist/studio/mcpMain.d.ts +1 -0
- package/dist/studio/mcpServer.d.ts +2 -0
- package/dist/studio/record.d.ts +54 -0
- package/dist/studio/run.d.ts +78 -0
- package/dist/studio/session.d.ts +80 -0
- package/dist/studio/timeline.d.ts +35 -0
- package/dist/studio/vitePlugin.d.ts +6 -0
- package/dist/studio-plugin.js +228 -0
- package/dist/ui/overlay.d.ts +6 -0
- package/dist/verify/audioFilmstrip.d.ts +39 -0
- package/dist/verify/ethnography.d.ts +67 -0
- package/dist/verify/gates.d.ts +160 -0
- package/dist/verify/layout.d.ts +30 -3
- package/dist/verify/ramp.d.ts +40 -0
- package/dist/world.d.ts +38 -2
- package/dist-studio/assets/index-C7tty_Wo.js +109 -0
- package/dist-studio/assets/index-CM3tjRQo.css +1 -0
- package/dist-studio/index.html +18 -0
- package/docs/API.md +233 -9
- package/docs/CONVENTIONS.md +223 -0
- package/docs/EMBED.md +85 -0
- package/docs/QUICKSTART.md +129 -0
- package/docs/STUDIO.md +97 -0
- package/docs/VERIFICATION.md +226 -0
- package/package.json +39 -6
package/dist/app/game.d.ts
CHANGED
|
@@ -2,6 +2,23 @@ import { World } from '../world';
|
|
|
2
2
|
import type { Node } from '../scene/node';
|
|
3
3
|
import { type InputLog, type InputMap } from '../input/actions';
|
|
4
4
|
import type { ClockConfig } from '../core/clock';
|
|
5
|
+
import { type TuningSpec, type TuningValues } from './tuning';
|
|
6
|
+
/**
|
|
7
|
+
* Optional boot splash. Rendered by the engine (so its colors are palette-guaranteed
|
|
8
|
+
* and never trip a contrast escape the way a hand-rolled loading <div> can) while
|
|
9
|
+
* `preload` runs. Pass `splash: false` to opt out entirely.
|
|
10
|
+
*/
|
|
11
|
+
export interface SplashConfig {
|
|
12
|
+
/** Splash headline (defaults to the game title). */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Background/foreground pair; defaults to a contrast-guaranteed pair from the game background. */
|
|
15
|
+
palette?: {
|
|
16
|
+
bg: string;
|
|
17
|
+
fg: string;
|
|
18
|
+
};
|
|
19
|
+
/** Keep the splash up at least this long, so a fast preload doesn't flash by. */
|
|
20
|
+
minDurationMs?: number;
|
|
21
|
+
}
|
|
5
22
|
export interface GameDefinition {
|
|
6
23
|
title: string;
|
|
7
24
|
width?: number;
|
|
@@ -14,11 +31,39 @@ export interface GameDefinition {
|
|
|
14
31
|
build(world: World): Node;
|
|
15
32
|
/** Optional compact probe snapshot for verification (defaults to World.probe). */
|
|
16
33
|
probe?(world: World): Record<string, unknown>;
|
|
34
|
+
/**
|
|
35
|
+
* Live-tunable parameters, declared once. Defaults ARE the config; Studio and
|
|
36
|
+
* tests override them via `createWorld(def, { tuning })` and the sim reads
|
|
37
|
+
* resolved values with `world.tune(key)`. Values are hashed + snapshotted.
|
|
38
|
+
*/
|
|
39
|
+
tuning?: TuningSpec;
|
|
40
|
+
/**
|
|
41
|
+
* Re-attach behaviors/controllers after `world.restore()` rebuilds the tree
|
|
42
|
+
* from data (closures do not survive a restore). One contract serves every
|
|
43
|
+
* carryover path: Studio knob changes, variant toggles, HMR, and net rollback.
|
|
44
|
+
*/
|
|
45
|
+
attach?(world: World): void;
|
|
46
|
+
/**
|
|
47
|
+
* Awaited before the world starts stepping — load fonts, sprite atlases, a
|
|
48
|
+
* SoundFont, anything async. The engine holds a splash on screen until it
|
|
49
|
+
* resolves, so there is no asset pop-in and no ungoverned pre-first-frame window.
|
|
50
|
+
*/
|
|
51
|
+
preload?(world: World): Promise<void>;
|
|
52
|
+
/** Boot splash config, or `false` to start on the first frame with no cover. */
|
|
53
|
+
splash?: SplashConfig | false;
|
|
17
54
|
}
|
|
18
55
|
/** Identity + defaults. Kept as a function so games read `export default defineGame({…})`. */
|
|
19
56
|
export declare function defineGame(def: GameDefinition): Required<Pick<GameDefinition, 'width' | 'height' | 'seed' | 'inputMap' | 'background'>> & GameDefinition;
|
|
20
|
-
|
|
21
|
-
|
|
57
|
+
export interface CreateWorldOptions {
|
|
58
|
+
seed?: number;
|
|
59
|
+
/** Overrides for declared tuning knobs (undeclared keys are dropped). */
|
|
60
|
+
tuning?: TuningValues;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build a live, deterministic World from a game definition. No browser needed.
|
|
64
|
+
* `opts` as a bare number is the legacy seed override.
|
|
65
|
+
*/
|
|
66
|
+
export declare function createWorld(def: GameDefinition, opts?: number | CreateWorldOptions): World;
|
|
22
67
|
/** The input map a game uses (with defaults applied). */
|
|
23
68
|
export declare function gameInputMap(def: GameDefinition): InputMap;
|
|
24
69
|
export interface HeadlessResult {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { GameDefinition } from './game';
|
|
2
|
+
export type TuningValue = number | string;
|
|
3
|
+
export type TuningValues = Record<string, TuningValue>;
|
|
4
|
+
export interface TuningKnob {
|
|
5
|
+
key: string;
|
|
6
|
+
/** Human label for panels; defaults to the key. */
|
|
7
|
+
label?: string;
|
|
8
|
+
type: 'number' | 'color' | 'enum';
|
|
9
|
+
default: TuningValue;
|
|
10
|
+
/** number knobs: slider bounds + resolution. */
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
step?: number;
|
|
14
|
+
/** enum knobs: the allowed values. */
|
|
15
|
+
options?: string[];
|
|
16
|
+
/** Panel grouping (e.g. 'jump', 'combat', 'style'). */
|
|
17
|
+
group?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Pure-view knob (palette, fonts): eligible for cheap re-apply without a sim
|
|
20
|
+
* rebuild. Sim-affecting knobs leave this unset.
|
|
21
|
+
*/
|
|
22
|
+
cosmetic?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface TuningSpec {
|
|
25
|
+
knobs: TuningKnob[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A named alternative of a game for A/B playtesting: a tuning preset and/or a
|
|
29
|
+
* definition patch. Tuning-only variants can hot-swap mid-play (rebuild with
|
|
30
|
+
* snapshot carryover); `patch` variants restart fresh.
|
|
31
|
+
*/
|
|
32
|
+
export interface Variant {
|
|
33
|
+
label: string;
|
|
34
|
+
tuning?: TuningValues;
|
|
35
|
+
patch?(def: GameDefinition): GameDefinition;
|
|
36
|
+
}
|
|
37
|
+
/** Sugar for declaring knobs without repeating the key in a label. */
|
|
38
|
+
export declare const knob: {
|
|
39
|
+
num(key: string, opts: {
|
|
40
|
+
default: number;
|
|
41
|
+
min?: number;
|
|
42
|
+
max?: number;
|
|
43
|
+
step?: number;
|
|
44
|
+
label?: string;
|
|
45
|
+
group?: string;
|
|
46
|
+
cosmetic?: boolean;
|
|
47
|
+
}): TuningKnob;
|
|
48
|
+
color(key: string, opts: {
|
|
49
|
+
default: string;
|
|
50
|
+
label?: string;
|
|
51
|
+
group?: string;
|
|
52
|
+
cosmetic?: boolean;
|
|
53
|
+
}): TuningKnob;
|
|
54
|
+
enumOf(key: string, opts: {
|
|
55
|
+
default: string;
|
|
56
|
+
options: string[];
|
|
57
|
+
label?: string;
|
|
58
|
+
group?: string;
|
|
59
|
+
cosmetic?: boolean;
|
|
60
|
+
}): TuningKnob;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Resolve a spec + overrides into concrete values: declared defaults first,
|
|
64
|
+
* overrides applied only for DECLARED keys (unknown keys are dropped — a typo'd
|
|
65
|
+
* override must not smuggle undeclared state into the hash), numbers clamped to
|
|
66
|
+
* [min, max] and coerced, enum values validated against `options`.
|
|
67
|
+
*/
|
|
68
|
+
export declare function resolveTuning(spec: TuningSpec | undefined, overrides?: TuningValues): TuningValues;
|
package/dist/art/palette.d.ts
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
import type { Rng } from '../core/rng';
|
|
2
|
+
/**
|
|
3
|
+
* The Kentō swatch set — the single source of truth every named palette derives
|
|
4
|
+
* from. Neutrals form one continuous ground→ink ramp; each of the eight hues has
|
|
5
|
+
* a `Deep` tone (holds AA as a large mark on light washi) and a bright tone (pops
|
|
6
|
+
* on dark sumi/kuro). Traditional Japanese color names keep it on-brand. Pick a
|
|
7
|
+
* hue by name the way the site does, or use a ready-made `Palette` below.
|
|
8
|
+
*/
|
|
9
|
+
export declare const KENTO: {
|
|
10
|
+
readonly gofun: "#f7f1e2";
|
|
11
|
+
readonly washi: "#efe7d3";
|
|
12
|
+
readonly kinu: "#e4d8bd";
|
|
13
|
+
readonly line: "#d8cbac";
|
|
14
|
+
readonly kinako: "#b9a882";
|
|
15
|
+
readonly stone: "#6c6252";
|
|
16
|
+
readonly sumiSoft: "#494133";
|
|
17
|
+
readonly sumi: "#23201a";
|
|
18
|
+
readonly kuro: "#181820";
|
|
19
|
+
readonly yohaku: "#12121a";
|
|
20
|
+
readonly darkLine: "#2c2c36";
|
|
21
|
+
readonly shuDeep: "#b23a24";
|
|
22
|
+
readonly shu: "#d9583c";
|
|
23
|
+
readonly kakiDeep: "#bf6a1c";
|
|
24
|
+
readonly kaki: "#e79a49";
|
|
25
|
+
readonly koDeep: "#94741d";
|
|
26
|
+
readonly ko: "#e3c054";
|
|
27
|
+
readonly matsuDeep: "#4a7a3a";
|
|
28
|
+
readonly matsu: "#8bad52";
|
|
29
|
+
readonly asagiDeep: "#2c7a90";
|
|
30
|
+
readonly asagi: "#57bad2";
|
|
31
|
+
readonly aiDeep: "#2b4257";
|
|
32
|
+
readonly ai: "#5a86ad";
|
|
33
|
+
readonly fujiDeep: "#63548c";
|
|
34
|
+
readonly fuji: "#a091cf";
|
|
35
|
+
readonly sakuDeep: "#b0506e";
|
|
36
|
+
readonly saku: "#e097ac";
|
|
37
|
+
};
|
|
2
38
|
export interface Palette {
|
|
3
39
|
name: string;
|
|
4
40
|
bg: string;
|
|
@@ -11,9 +47,14 @@ export interface Palette {
|
|
|
11
47
|
warn: string;
|
|
12
48
|
/** A small ordered ramp for categorical fills. */
|
|
13
49
|
ramp: string[];
|
|
50
|
+
/** The full on-brand swatch set, for games that pick hues by name. */
|
|
51
|
+
swatches?: typeof KENTO;
|
|
14
52
|
}
|
|
53
|
+
/** Default light woodblock palette — washi ground, sumi ink, deep accents. */
|
|
15
54
|
export declare const MEADOW: Palette;
|
|
55
|
+
/** Dark counterpart — kuro ground, gofun ink, the same hues in their bright tone. */
|
|
16
56
|
export declare const DUSK: Palette;
|
|
57
|
+
/** Higher-key paper variant — brightest ground, same hue family. */
|
|
17
58
|
export declare const PAPER: Palette;
|
|
18
59
|
export declare const PALETTES: Record<string, Palette>;
|
|
19
60
|
/** Blend two hex colors (t in [0,1]). Deterministic, no allocations of note. */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/** A breakpoint on an RTPC curve: at input `x`, the target is `y`. */
|
|
2
|
+
export interface Breakpoint {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Evaluate a piecewise-linear RTPC curve at `x`. Breakpoints are sorted by x;
|
|
8
|
+
* values outside the range clamp to the nearest endpoint. This one primitive
|
|
9
|
+
* drives volume, filter cutoff, layer gain, and distance in a uniform, testable
|
|
10
|
+
* way — exactly what a Wwise RTPC / FMOD parameter automation does.
|
|
11
|
+
*/
|
|
12
|
+
export declare function evalCurve(curve: Breakpoint[], x: number): number;
|
|
13
|
+
export type DistanceModel = 'linear' | 'inverse' | 'exponential';
|
|
14
|
+
/**
|
|
15
|
+
* Distance-attenuation gain, matching the Web Audio PannerNode formulas exactly
|
|
16
|
+
* so an offline render agrees with live playback. `refDistance` is full volume,
|
|
17
|
+
* `maxDistance` is the floor, `rolloff` shapes the falloff.
|
|
18
|
+
*/
|
|
19
|
+
export declare function distanceGain(model: DistanceModel, distance: number, refDistance?: number, maxDistance?: number, rolloff?: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Equal-power stereo pan from a horizontal offset. dx is the source's screen-x
|
|
22
|
+
* relative to the listener; `spread` is the half-width at which pan saturates.
|
|
23
|
+
*/
|
|
24
|
+
export declare function panFromOffset(dx: number, spread?: number): number;
|
|
25
|
+
/**
|
|
26
|
+
* A positional cue's mix: distance-attenuated gain × pan. Combines the two
|
|
27
|
+
* above into what a 2-D game actually needs. `hearing` is the cutoff beyond
|
|
28
|
+
* which the source is inaudible.
|
|
29
|
+
*/
|
|
30
|
+
export interface SpatialMix {
|
|
31
|
+
gain: number;
|
|
32
|
+
pan: number;
|
|
33
|
+
audible: boolean;
|
|
34
|
+
}
|
|
35
|
+
export declare function spatialMix(dx: number, dy: number, hearing?: number, rolloff?: number): SpatialMix;
|
|
36
|
+
/** A named layer of vertical (stacked) adaptive music. */
|
|
37
|
+
export interface MusicLayer {
|
|
38
|
+
name: string;
|
|
39
|
+
/** The layer fades in from `fadeIn` and reaches full gain by `full` intensity. */
|
|
40
|
+
fadeIn: number;
|
|
41
|
+
full: number;
|
|
42
|
+
/** Optional cap on this layer's gain (default 1). */
|
|
43
|
+
maxGain?: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Vertical layering: given an intensity value (0..1, e.g. combat proximity),
|
|
47
|
+
* return each layer's gain. Layers fade in at their thresholds so texture
|
|
48
|
+
* thickens with intensity without ever interrupting the flow — the single
|
|
49
|
+
* biggest adaptive-music payoff per line, and trivially deterministic.
|
|
50
|
+
*/
|
|
51
|
+
export declare function layerGains(layers: MusicLayer[], intensity: number): Record<string, number>;
|
|
52
|
+
/**
|
|
53
|
+
* A ducking envelope target: while a higher-priority bus (dialogue, a stinger)
|
|
54
|
+
* is active, the ducked bus drops by `amount` dB. Returns a linear gain the
|
|
55
|
+
* driver ramps toward with setTargetAtTime — deterministic because it's keyed
|
|
56
|
+
* off game state, not an audio sidechain.
|
|
57
|
+
*/
|
|
58
|
+
export declare function duckGain(active: boolean, amountDb?: number): number;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Song } from './music';
|
|
2
|
+
export interface AlbumTrack {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
intent: string;
|
|
6
|
+
make: () => Song;
|
|
7
|
+
}
|
|
8
|
+
/** The album. */
|
|
9
|
+
export declare const ALBUM: {
|
|
10
|
+
title: string;
|
|
11
|
+
subtitle: string;
|
|
12
|
+
concept: string;
|
|
13
|
+
tracks: AlbumTrack[];
|
|
14
|
+
};
|
|
15
|
+
/** Look up an album track by id. */
|
|
16
|
+
export declare function albumTrack(id: string): AlbumTrack | undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { type Samples } from './pcm';
|
|
2
|
+
/** Amplitude ratio → decibels full-scale. Silence maps to -Infinity. */
|
|
3
|
+
export declare function dbfs(amp: number): number;
|
|
4
|
+
/** Root-mean-square level of a signal. */
|
|
5
|
+
export declare function rms(sig: Samples): number;
|
|
6
|
+
/** Peak absolute sample. */
|
|
7
|
+
export declare function peakAmp(sig: Samples): number;
|
|
8
|
+
/** Zero crossings per second — a rough "noisiness / brightness" proxy. */
|
|
9
|
+
export declare function zcr(sig: Samples, sampleRate?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* In-place iterative radix-2 Cooley–Tukey FFT. `re`/`im` length must be a power
|
|
12
|
+
* of two. Deterministic: twiddle factors come from dcos/dsin.
|
|
13
|
+
*/
|
|
14
|
+
export declare function fft(re: Float32Array, im: Float32Array): void;
|
|
15
|
+
/** Hann-windowed magnitude spectrum of a frame (length rounded down to pow2). */
|
|
16
|
+
export declare function magnitudeSpectrum(frame: Samples): Float32Array;
|
|
17
|
+
/**
|
|
18
|
+
* Spectral centroid (Hz) — the "center of mass" of the spectrum, a robust
|
|
19
|
+
* brightness measure. Averaged over Hann frames across the whole signal.
|
|
20
|
+
*/
|
|
21
|
+
export declare function spectralCentroid(sig: Samples, sampleRate?: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Spectral flux onset envelope: per-frame sum of positive magnitude increases.
|
|
24
|
+
* Peaks mark note onsets — the basis for tempo/onset-density features.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fluxEnvelope(sig: Samples, frameSize?: number, hop?: number): Float32Array;
|
|
27
|
+
/** Onsets per second — busy/percussive vs sparse/sustained. */
|
|
28
|
+
export declare function onsetDensity(sig: Samples, sampleRate?: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Estimate tempo (BPM) by autocorrelating the flux onset envelope and picking
|
|
31
|
+
* the strongest lag in the 60–200 BPM band. Returns 0 if indeterminate.
|
|
32
|
+
*/
|
|
33
|
+
export declare function estimateTempo(sig: Samples, sampleRate?: number): number;
|
|
34
|
+
/** Crest factor in dB (peak / rms). Music usually lives ~8–20 dB; a tiny value
|
|
35
|
+
* means it's squashed/distorted, a huge value means it's mostly silence. */
|
|
36
|
+
export declare function crestFactorDb(sig: Samples): number;
|
|
37
|
+
/** Fractional energy in coarse frequency bands — the spectral balance of a mix.
|
|
38
|
+
* Returns fractions (summing to ~1) for sub / bass / lowMid / mid / high / air. */
|
|
39
|
+
export interface BandBalance {
|
|
40
|
+
sub: number;
|
|
41
|
+
bass: number;
|
|
42
|
+
lowMid: number;
|
|
43
|
+
mid: number;
|
|
44
|
+
high: number;
|
|
45
|
+
air: number;
|
|
46
|
+
}
|
|
47
|
+
export declare function bandBalance(sig: Samples, sampleRate?: number): BandBalance;
|
|
48
|
+
/** A compact numeric fingerprint of a signal — the audio-filmstrip's assertions. */
|
|
49
|
+
export interface AudioFeatures {
|
|
50
|
+
durationSec: number;
|
|
51
|
+
rms: number;
|
|
52
|
+
peakDb: number;
|
|
53
|
+
centroidHz: number;
|
|
54
|
+
zcr: number;
|
|
55
|
+
onsetDensity: number;
|
|
56
|
+
tempoBpm: number;
|
|
57
|
+
}
|
|
58
|
+
/** Extract the full feature vector used by verification. */
|
|
59
|
+
export declare function features(sig: Samples, sampleRate?: number): AudioFeatures;
|
package/dist/audio/audio.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type SoundSpec } from './synth';
|
|
2
|
+
import { type Song } from './music';
|
|
1
3
|
export interface Volumes {
|
|
2
4
|
master: number;
|
|
3
5
|
music: number;
|
|
@@ -37,6 +39,25 @@ export declare class AudioBus {
|
|
|
37
39
|
spatial(freq: number, dx: number, dist: number, hearing?: number, duration?: number, type?: OscillatorType): void;
|
|
38
40
|
/** Play a sequence of tones as an arpeggio/chord. */
|
|
39
41
|
play(tones: Tone[]): void;
|
|
42
|
+
/**
|
|
43
|
+
* Play a data-defined SoundSpec through the SFX bus. The spec is rendered to
|
|
44
|
+
* samples (deterministically, at the context rate) and played as an
|
|
45
|
+
* AudioBuffer — the same SoundSpec that a verify suite proves headlessly.
|
|
46
|
+
* No-op without an AudioContext.
|
|
47
|
+
*/
|
|
48
|
+
playSpec(spec: SoundSpec, opts?: {
|
|
49
|
+
pan?: number;
|
|
50
|
+
gain?: number;
|
|
51
|
+
when?: number;
|
|
52
|
+
}): void;
|
|
53
|
+
/**
|
|
54
|
+
* Render a Song offline and play it on the music bus. Returns a stop handle.
|
|
55
|
+
* `loop` repeats the musical body (excluding the ring-out tail). No-op without
|
|
56
|
+
* an AudioContext.
|
|
57
|
+
*/
|
|
58
|
+
playSong(song: Song, opts?: {
|
|
59
|
+
loop?: boolean;
|
|
60
|
+
}): () => void;
|
|
40
61
|
/** Convenience SFX. */
|
|
41
62
|
blip(freq?: number): void;
|
|
42
63
|
chime(): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a chord symbol to MIDI notes at the given octave (default 4). Supports
|
|
3
|
+
* a slash bass ("C/E", "Dm7/G"). Throws on an unknown quality so authoring
|
|
4
|
+
* mistakes surface instead of sounding wrong.
|
|
5
|
+
*/
|
|
6
|
+
export declare function chordNotes(symbol: string, octave?: number): number[];
|
|
7
|
+
/**
|
|
8
|
+
* Rootless voicing: drop the root (the bass covers it) and keep the colour
|
|
9
|
+
* tones (3rd, 7th, extensions) in a tight register — the authentic sound of
|
|
10
|
+
* jazz-piano/Rhodes comping. Optionally recenter near `targetMidi`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function rootless(chord: number[], targetMidi?: number): number[];
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a whole progression of chord symbols to note sets. Convenience for
|
|
15
|
+
* writing changes as a string array: chordChanges(['Dm7','G7','Cmaj7']).
|
|
16
|
+
*/
|
|
17
|
+
export declare function chordChanges(symbols: string[], octave?: number): number[][];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Song } from './music';
|
|
2
|
+
export interface GenreTrack {
|
|
3
|
+
id: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
make: () => Song;
|
|
7
|
+
}
|
|
8
|
+
/** The genre songbook — the demo set. */
|
|
9
|
+
export declare const GENRES: GenreTrack[];
|
|
10
|
+
/** Look up a genre by id. */
|
|
11
|
+
export declare function genre(id: string): GenreTrack | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ScaleName } from './theory';
|
|
2
|
+
import type { Song, Pitch } from './music';
|
|
3
|
+
export interface MusicLintResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
/** Hard failures: malformed data that would render wrong or crash. */
|
|
6
|
+
errors: string[];
|
|
7
|
+
/** Musical smells: renders fine, but likely not what was intended. */
|
|
8
|
+
warnings: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface LintOptions {
|
|
11
|
+
/** If given, flag notes outside this key/mode as out-of-key warnings. */
|
|
12
|
+
key?: {
|
|
13
|
+
tonic: Pitch;
|
|
14
|
+
mode: ScaleName;
|
|
15
|
+
};
|
|
16
|
+
/** Max fraction of out-of-key notes tolerated before it's an error (default 1 = never). */
|
|
17
|
+
maxOutOfKey?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Lint a Song. Errors block (bad bpm, empty tracks, unparseable pitches, bad
|
|
21
|
+
* pattern indices, non-positive durations, out-of-MIDI-range pitches). Warnings
|
|
22
|
+
* flag musical smells (out-of-key notes, no structural repetition).
|
|
23
|
+
*/
|
|
24
|
+
export declare function lintSong(song: Song, opts?: LintOptions): MusicLintResult;
|
|
25
|
+
/**
|
|
26
|
+
* Check that a roman-numeral progression resolves to tonic — a real cadence,
|
|
27
|
+
* not a phrase left hanging. Accepts an ending on I/i, or a V(7)→I/i cadence.
|
|
28
|
+
*/
|
|
29
|
+
export declare function cadenceResolves(romans: string[]): boolean;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AudioFeatures } from './analysis';
|
|
2
|
+
/** Per-feature normalization scales — the "how much is a lot" for each axis. */
|
|
3
|
+
interface FeatureWeights {
|
|
4
|
+
tempoBpm: number;
|
|
5
|
+
centroidHz: number;
|
|
6
|
+
rms: number;
|
|
7
|
+
zcr: number;
|
|
8
|
+
onsetDensity: number;
|
|
9
|
+
peakDb: number;
|
|
10
|
+
}
|
|
11
|
+
export interface FeatureDelta {
|
|
12
|
+
feature: keyof FeatureWeights;
|
|
13
|
+
reference: number;
|
|
14
|
+
candidate: number;
|
|
15
|
+
/** Normalized signed error (candidate − reference) / scale. */
|
|
16
|
+
normalized: number;
|
|
17
|
+
/** Human/LLM-actionable direction, e.g. "too dark", "too fast". */
|
|
18
|
+
note: string;
|
|
19
|
+
}
|
|
20
|
+
export interface MatchResult {
|
|
21
|
+
/** Overall distance (RMS of normalized per-feature errors). 0 = identical. */
|
|
22
|
+
distance: number;
|
|
23
|
+
/** Per-feature breakdown, largest error first. */
|
|
24
|
+
deltas: FeatureDelta[];
|
|
25
|
+
/** The single most-off feature's note, or 'close match'. */
|
|
26
|
+
headline: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Compare a candidate feature vector to a reference. Returns an overall
|
|
30
|
+
* distance plus a per-feature, direction-aware breakdown sorted by severity.
|
|
31
|
+
*/
|
|
32
|
+
export declare function featureDistance(reference: AudioFeatures, candidate: AudioFeatures, weights?: Partial<FeatureWeights>): MatchResult;
|
|
33
|
+
/**
|
|
34
|
+
* A compact, printable report of the mismatch — the "shortcomings" surface an
|
|
35
|
+
* AI loop reads to decide its next move.
|
|
36
|
+
*/
|
|
37
|
+
export declare function matchReport(reference: AudioFeatures, candidate: AudioFeatures): string;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { type SoundSpec } from './synth';
|
|
2
|
+
import { type ReverbOptions } from './reverb';
|
|
3
|
+
import { type StereoBuffer } from './pcm';
|
|
4
|
+
/** A voice: everything a SoundSpec has except pitch, which each note supplies. */
|
|
5
|
+
export type Instrument = Omit<SoundSpec, 'freq'>;
|
|
6
|
+
export type Pitch = string | number;
|
|
7
|
+
/** One note (or chord, or rest) in a pattern. */
|
|
8
|
+
export interface Note {
|
|
9
|
+
/** A pitch, a chord (array of pitches), or null for a rest. */
|
|
10
|
+
pitch: Pitch | Pitch[] | null;
|
|
11
|
+
/** Duration in beats. */
|
|
12
|
+
beats: number;
|
|
13
|
+
/** Velocity 0..1 (default 1). */
|
|
14
|
+
vel?: number;
|
|
15
|
+
}
|
|
16
|
+
/** A monophonic-per-note lane: an instrument playing sequenced patterns. */
|
|
17
|
+
export interface Track {
|
|
18
|
+
name?: string;
|
|
19
|
+
instrument: Instrument;
|
|
20
|
+
/** Reusable note patterns, referenced by index in `sequence`. */
|
|
21
|
+
patterns: Note[][];
|
|
22
|
+
/** Order in which patterns play. */
|
|
23
|
+
sequence: number[];
|
|
24
|
+
/** Track mix level (default 0.7). */
|
|
25
|
+
gain?: number;
|
|
26
|
+
/** Stereo position -1..1 (default 0). */
|
|
27
|
+
pan?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface Song {
|
|
30
|
+
bpm: number;
|
|
31
|
+
tracks: Track[];
|
|
32
|
+
/** Concert-A reference (default 440). */
|
|
33
|
+
a4?: number;
|
|
34
|
+
/** Tail seconds appended so release/echo tails ring out (default 2). */
|
|
35
|
+
tailSec?: number;
|
|
36
|
+
/** Room reverb applied to the whole mix (space + glue). */
|
|
37
|
+
reverb?: ReverbOptions;
|
|
38
|
+
/** Swing 0..1 — delays off-beat 8th notes for a shuffled feel (jazz/lofi). */
|
|
39
|
+
swing?: number;
|
|
40
|
+
/** Humanize 0..1 — small seeded timing/velocity jitter so it doesn't feel robotic. */
|
|
41
|
+
humanize?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Velocity→brightness 0..1 — how much a soft note darkens (like a real
|
|
44
|
+
* instrument: harder = brighter). Scales each voice's lowpass by velocity.
|
|
45
|
+
* Default 0.5. This is what turns a velocity map into actual phrasing.
|
|
46
|
+
*/
|
|
47
|
+
velBrightness?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Sidechain "pump": a beat-synced ducking of the whole mix — the breathing
|
|
50
|
+
* signature of electronic music. depth 0..1, beatsPerCycle sets the pulse
|
|
51
|
+
* (default 1 = every beat).
|
|
52
|
+
*/
|
|
53
|
+
sidechain?: {
|
|
54
|
+
depth?: number;
|
|
55
|
+
beatsPerCycle?: number;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Master bus (a light mastering pass): `lowCut` tucks boomy sub/bass, and
|
|
59
|
+
* `presence`/`air` lift high-mid clarity and top-end sparkle. `compress`
|
|
60
|
+
* (0..1) applies a gentle glue compressor with makeup gain — evens dynamics
|
|
61
|
+
* and lifts perceived loudness (esp. sparse ballads). Each field 0..1.
|
|
62
|
+
*/
|
|
63
|
+
master?: {
|
|
64
|
+
lowCut?: number;
|
|
65
|
+
presence?: number;
|
|
66
|
+
air?: number;
|
|
67
|
+
compress?: number;
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/** Total musical length of a song in beats (the longest track). */
|
|
71
|
+
export declare function songBeats(song: Song): number;
|
|
72
|
+
/** Song duration in seconds, excluding the ring-out tail. */
|
|
73
|
+
export declare function songDuration(song: Song): number;
|
|
74
|
+
/**
|
|
75
|
+
* Render a Song to a stereo buffer. Deterministic: a per-track seeded Rng feeds
|
|
76
|
+
* any noise voices, so the same Song always produces the same mix. Notes are
|
|
77
|
+
* synthesized via renderSound with the track's instrument, the note's pitch and
|
|
78
|
+
* velocity, and a sustain sized to the note's beat length.
|
|
79
|
+
*/
|
|
80
|
+
export declare function renderSong(song: Song, opts?: {
|
|
81
|
+
sampleRate?: number;
|
|
82
|
+
normalizePeak?: number;
|
|
83
|
+
}): StereoBuffer;
|
|
84
|
+
/**
|
|
85
|
+
* A small library of ready-to-use instrument voices. These are the "adopt a
|
|
86
|
+
* timbre" defaults so a composer starts from something musical, then tweaks.
|
|
87
|
+
*/
|
|
88
|
+
export declare const INSTRUMENTS: Record<string, Instrument>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** Canonical render rate. 44.1 kHz — CD quality, ubiquitous. */
|
|
2
|
+
export declare const SAMPLE_RATE = 44100;
|
|
3
|
+
/** A mono signal: Float32 samples, nominally in [-1, 1], at some sample rate. */
|
|
4
|
+
export type Samples = Float32Array;
|
|
5
|
+
/** A stereo buffer: two channels sharing one sample rate. The mix target. */
|
|
6
|
+
export interface StereoBuffer {
|
|
7
|
+
sampleRate: number;
|
|
8
|
+
left: Float32Array;
|
|
9
|
+
right: Float32Array;
|
|
10
|
+
}
|
|
11
|
+
/** Allocate a silent stereo buffer of the given duration. */
|
|
12
|
+
export declare function createStereo(durationSec: number, sampleRate?: number): StereoBuffer;
|
|
13
|
+
/** Frame count (samples per channel). */
|
|
14
|
+
export declare function stereoFrames(buf: StereoBuffer): number;
|
|
15
|
+
/** Duration in seconds. */
|
|
16
|
+
export declare function stereoDuration(buf: StereoBuffer): number;
|
|
17
|
+
/**
|
|
18
|
+
* Equal-power pan law. pan ∈ [-1, 1]: -1 = hard left, 0 = center, +1 = hard
|
|
19
|
+
* right. Uses a quarter-sine curve so total power is constant across the sweep
|
|
20
|
+
* (a linear pan would dip −3 dB in the middle).
|
|
21
|
+
*/
|
|
22
|
+
export declare function panGains(pan: number): {
|
|
23
|
+
l: number;
|
|
24
|
+
r: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Mix a mono signal into a stereo bus at a start sample, with gain and pan.
|
|
28
|
+
* Additive — many voices accumulate into one bus. Samples outside the bus are
|
|
29
|
+
* clipped away (no reallocation), so callers size the bus for the whole score.
|
|
30
|
+
*/
|
|
31
|
+
export declare function mixMono(bus: StereoBuffer, sig: Samples, startSample: number, gain?: number, pan?: number): void;
|
|
32
|
+
/** Largest absolute sample across both channels — the true peak. */
|
|
33
|
+
export declare function peak(buf: StereoBuffer): number;
|
|
34
|
+
/**
|
|
35
|
+
* Transparent soft-clip. Unity slope at the origin (quiet signals pass through
|
|
36
|
+
* untouched) and asymptotes to ±1, so it tames peaks without hard-clipping and
|
|
37
|
+
* without boosting quiet material. Only sqrt — stays deterministic.
|
|
38
|
+
*/
|
|
39
|
+
export declare function softClip(x: number): number;
|
|
40
|
+
/** Soft-clip a whole buffer in place. Tames additive overshoot on a busy mix. */
|
|
41
|
+
export declare function softClipInPlace(buf: StereoBuffer): void;
|
|
42
|
+
/** Scale a buffer in place so its peak sits at `target` (default −1 dBFS-ish). */
|
|
43
|
+
export declare function normalize(buf: StereoBuffer, target?: number): void;
|
|
44
|
+
/**
|
|
45
|
+
* Encode a stereo buffer as a 16-bit PCM WAV (RIFF) byte array — a real,
|
|
46
|
+
* playable artifact a human (or a golden-file test) can keep. Zero-dependency.
|
|
47
|
+
*/
|
|
48
|
+
export declare function encodeWav(buf: StereoBuffer): Uint8Array;
|
|
49
|
+
/**
|
|
50
|
+
* Deterministic 32-bit fingerprint of a signal — samples quantized to 16-bit
|
|
51
|
+
* then FNV-1a hashed. Lets a test pin "this synth produces exactly this sound"
|
|
52
|
+
* without storing the whole buffer, and detect any DSP regression.
|
|
53
|
+
*/
|
|
54
|
+
export declare function signalHash(sig: Samples): number;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { StereoBuffer } from './pcm';
|
|
2
|
+
/** Stereo width via mid/side energy ratio. 0 = mono, ~0.3+ = a wide mix. */
|
|
3
|
+
export declare function stereoWidth(buf: StereoBuffer): number;
|
|
4
|
+
type Range = [number, number];
|
|
5
|
+
export interface GenreProfile {
|
|
6
|
+
tempo: Range;
|
|
7
|
+
centroid: Range;
|
|
8
|
+
onsets: Range;
|
|
9
|
+
rms: Range;
|
|
10
|
+
crestDb: Range;
|
|
11
|
+
width: Range;
|
|
12
|
+
/** Max acceptable low-mid (250–800 Hz) fraction — warm genres tolerate more. */
|
|
13
|
+
maxMud?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Target windows per genre — set to real genre norms for a balanced mix. */
|
|
16
|
+
export declare const GENRE_PROFILES: Record<string, GenreProfile>;
|
|
17
|
+
export interface QualityScore {
|
|
18
|
+
score: number;
|
|
19
|
+
dims: Record<string, number>;
|
|
20
|
+
notes: string[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Score a rendered track against a genre profile. Weighted mean of dimensions,
|
|
24
|
+
* ×100. `notes` lists any dimension below a comfortable threshold with the
|
|
25
|
+
* measured value and direction, so a composer knows exactly what to fix.
|
|
26
|
+
*/
|
|
27
|
+
export declare function scoreTrack(buf: StereoBuffer, profile: GenreProfile): QualityScore;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StereoBuffer } from './pcm';
|
|
2
|
+
export interface ReverbOptions {
|
|
3
|
+
/** Wet/dry mix 0..1 (default 0.25). */
|
|
4
|
+
wet?: number;
|
|
5
|
+
/** Tail length / feedback 0..1 — bigger = longer, more cathedral (default 0.7). */
|
|
6
|
+
roomSize?: number;
|
|
7
|
+
/** High-frequency damping 0..1 — bigger = darker tail (default 0.5). */
|
|
8
|
+
damp?: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Apply reverb to a stereo buffer in place. A mono send (the L/R sum) feeds two
|
|
12
|
+
* slightly offset comb/allpass banks to produce a stereo tail, mixed back with
|
|
13
|
+
* the dry signal. Deterministic — pure delay-line arithmetic.
|
|
14
|
+
*/
|
|
15
|
+
export declare function applyReverb(buf: StereoBuffer, opts?: ReverbOptions): void;
|