hayao 0.1.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 +127 -21
- 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/autotile.d.ts +77 -0
- package/dist/art/bitmapFont.d.ts +113 -0
- package/dist/art/font5.d.ts +11 -0
- package/dist/art/palette.d.ts +79 -0
- package/dist/art/texture.d.ts +78 -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 +28 -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/dsl.d.ts +61 -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 +22 -0
- package/dist/core/math.d.ts +2 -0
- package/dist/hayao.global.js +15 -0
- package/dist/index.d.ts +66 -1
- package/dist/index.js +9174 -835
- package/dist/index.js.map +4 -4
- package/dist/index.min.js +15 -0
- package/dist/input/source.d.ts +52 -1
- package/dist/logic/fsm.d.ts +85 -0
- package/dist/logic/graph.d.ts +88 -0
- package/dist/logic/history.d.ts +54 -0
- package/dist/logic/random.d.ts +32 -0
- package/dist/mcp.js +31225 -0
- package/dist/net/browser.d.ts +37 -0
- package/dist/net/inputBuffer.d.ts +27 -0
- package/dist/net/lockstep.d.ts +79 -0
- package/dist/net/players.d.ts +27 -0
- package/dist/net/protocol.d.ts +100 -0
- package/dist/net/rollback.d.ts +89 -0
- package/dist/net/room.d.ts +78 -0
- package/dist/net/transport.d.ts +78 -0
- package/dist/persist/codec.d.ts +4 -0
- package/dist/persist/save.d.ts +32 -0
- package/dist/persist/storage.d.ts +46 -0
- package/dist/physics/rigidBody.d.ts +104 -0
- package/dist/physics/rigidCollide.d.ts +16 -0
- package/dist/physics/rigidJoints.d.ts +65 -0
- package/dist/physics/rigidQueries.d.ts +15 -0
- package/dist/physics/rigidStep.d.ts +14 -0
- package/dist/procgen/cave.d.ts +21 -0
- package/dist/procgen/grid.d.ts +21 -0
- package/dist/procgen/rooms.d.ts +34 -0
- package/dist/procgen/scatter.d.ts +32 -0
- package/dist/procgen/terrain.d.ts +24 -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/nineSlice.d.ts +32 -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/floatingText.d.ts +51 -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/scene/particles.d.ts +64 -0
- package/dist/scene/pool.d.ts +16 -0
- package/dist/scene/tween.d.ts +26 -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/ui/transition.d.ts +107 -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 +66 -0
- 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 +473 -10
- 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 +66 -11
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;
|
|
@@ -11,6 +13,8 @@ export interface Tone {
|
|
|
11
13
|
type?: OscillatorType;
|
|
12
14
|
gain?: number;
|
|
13
15
|
delay?: number;
|
|
16
|
+
/** Stereo position -1 (left) … 1 (right) — the spatial-audio hook. */
|
|
17
|
+
pan?: number;
|
|
14
18
|
}
|
|
15
19
|
export declare class AudioBus {
|
|
16
20
|
private ctx;
|
|
@@ -28,8 +32,32 @@ export declare class AudioBus {
|
|
|
28
32
|
private applyVolumes;
|
|
29
33
|
/** Play a single tone (no-op if audio unstarted). */
|
|
30
34
|
tone(spec: Tone): void;
|
|
35
|
+
/**
|
|
36
|
+
* A positional cue: gain falls with distance, pan follows the horizontal
|
|
37
|
+
* offset. dx/dist in design-space px; hearing range sets the falloff.
|
|
38
|
+
*/
|
|
39
|
+
spatial(freq: number, dx: number, dist: number, hearing?: number, duration?: number, type?: OscillatorType): void;
|
|
31
40
|
/** Play a sequence of tones as an arpeggio/chord. */
|
|
32
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;
|
|
33
61
|
/** Convenience SFX. */
|
|
34
62
|
blip(freq?: number): void;
|
|
35
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;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Rng } from '../core/rng';
|
|
2
|
+
import { type Samples } from './pcm';
|
|
3
|
+
export type Wave = 'sine' | 'triangle' | 'saw' | 'square' | 'noise';
|
|
4
|
+
/**
|
|
5
|
+
* A one-shot sound, fully described by data. Every field is optional; omitted
|
|
6
|
+
* fields take musical defaults. Times are seconds, pitch offsets are semitones,
|
|
7
|
+
* depths are 0..1 unless noted.
|
|
8
|
+
*/
|
|
9
|
+
export interface SoundSpec {
|
|
10
|
+
/** Base pitch in Hz. */
|
|
11
|
+
freq?: number;
|
|
12
|
+
/** Oscillator shape. */
|
|
13
|
+
wave?: Wave;
|
|
14
|
+
/** Square/pulse duty cycle 0..1 (square only). */
|
|
15
|
+
duty?: number;
|
|
16
|
+
attack?: number;
|
|
17
|
+
decay?: number;
|
|
18
|
+
sustain?: number;
|
|
19
|
+
release?: number;
|
|
20
|
+
sustainLevel?: number;
|
|
21
|
+
/** Percussive volume spike at onset (jsfxr "punch"), 0..1. */
|
|
22
|
+
punch?: number;
|
|
23
|
+
/** Overall output gain. */
|
|
24
|
+
volume?: number;
|
|
25
|
+
slide?: number;
|
|
26
|
+
slideAccel?: number;
|
|
27
|
+
pitchJump?: number;
|
|
28
|
+
pitchJumpTime?: number;
|
|
29
|
+
vibrato?: number;
|
|
30
|
+
vibratoFreq?: number;
|
|
31
|
+
/** Second detuned oscillator, cents (0 = off). Adds unison warmth/chorus. */
|
|
32
|
+
detune?: number;
|
|
33
|
+
/** Sub-oscillator: a sine one octave down, mixed at this level 0..1. Adds weight. */
|
|
34
|
+
sub?: number;
|
|
35
|
+
noise?: number;
|
|
36
|
+
shapeCurve?: number;
|
|
37
|
+
fm?: number;
|
|
38
|
+
fmFreq?: number;
|
|
39
|
+
tremolo?: number;
|
|
40
|
+
tremoloFreq?: number;
|
|
41
|
+
bitCrush?: number;
|
|
42
|
+
lowpass?: number;
|
|
43
|
+
highpass?: number;
|
|
44
|
+
delay?: number;
|
|
45
|
+
delayFeedback?: number;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Render a SoundSpec to a mono signal. Pure and deterministic: pass a seeded
|
|
49
|
+
* `rng` for reproducible noise (defaults to a fixed seed so bare specs are
|
|
50
|
+
* still bit-stable). The returned buffer is normalized only by `volume`, so a
|
|
51
|
+
* caller can mix many at chosen gains.
|
|
52
|
+
*/
|
|
53
|
+
export declare function renderSound(spec: SoundSpec, opts?: {
|
|
54
|
+
rng?: Rng;
|
|
55
|
+
sampleRate?: number;
|
|
56
|
+
}): Samples;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse scientific pitch notation ("C4", "F#3", "Bb5", "A4") to a MIDI number.
|
|
3
|
+
* Middle C (C4) = 60, A4 = 69. Throws on malformed input so bad authoring is
|
|
4
|
+
* caught, not silently mis-sounded.
|
|
5
|
+
*/
|
|
6
|
+
export declare function noteToMidi(name: string): number;
|
|
7
|
+
/** MIDI number → name in sharps, e.g. 60 → "C4". */
|
|
8
|
+
export declare function midiToName(midi: number): string;
|
|
9
|
+
/** MIDI number → frequency in Hz (A4 = a4, default 440). Deterministic. */
|
|
10
|
+
export declare function midiToFreq(midi: number, a4?: number): number;
|
|
11
|
+
/** Convenience: pitch name or MIDI number → Hz. */
|
|
12
|
+
export declare function pitchToFreq(pitch: string | number, a4?: number): number;
|
|
13
|
+
/** Scale (mode) interval tables in semitones from the tonic. */
|
|
14
|
+
export declare const SCALES: {
|
|
15
|
+
readonly major: readonly [0, 2, 4, 5, 7, 9, 11];
|
|
16
|
+
readonly minor: readonly [0, 2, 3, 5, 7, 8, 10];
|
|
17
|
+
readonly harmonicMinor: readonly [0, 2, 3, 5, 7, 8, 11];
|
|
18
|
+
readonly melodicMinor: readonly [0, 2, 3, 5, 7, 9, 11];
|
|
19
|
+
readonly dorian: readonly [0, 2, 3, 5, 7, 9, 10];
|
|
20
|
+
readonly phrygian: readonly [0, 1, 3, 5, 7, 8, 10];
|
|
21
|
+
readonly lydian: readonly [0, 2, 4, 6, 7, 9, 11];
|
|
22
|
+
readonly mixolydian: readonly [0, 2, 4, 5, 7, 9, 10];
|
|
23
|
+
readonly locrian: readonly [0, 1, 3, 5, 6, 8, 10];
|
|
24
|
+
readonly majorPentatonic: readonly [0, 2, 4, 7, 9];
|
|
25
|
+
readonly minorPentatonic: readonly [0, 3, 5, 7, 10];
|
|
26
|
+
readonly blues: readonly [0, 3, 5, 6, 7, 10];
|
|
27
|
+
readonly chromatic: readonly [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11];
|
|
28
|
+
};
|
|
29
|
+
export type ScaleName = keyof typeof SCALES;
|
|
30
|
+
/**
|
|
31
|
+
* Scale degrees as MIDI numbers spanning `octaves` starting at `tonic`.
|
|
32
|
+
* e.g. scaleMidis(noteToMidi('C4'), 'major', 2) → C4 D4 E4 … up two octaves.
|
|
33
|
+
*/
|
|
34
|
+
export declare function scaleMidis(tonic: number, mode?: ScaleName, octaves?: number): number[];
|
|
35
|
+
/** Pitch classes (0–11) of a scale — for in-key checks. */
|
|
36
|
+
export declare function scalePitchClasses(tonic: number, mode?: ScaleName): Set<number>;
|
|
37
|
+
/**
|
|
38
|
+
* Build a diatonic chord by stacking thirds on a scale degree. `degree` is
|
|
39
|
+
* 0-based (0 = I/i). Returns MIDI numbers. `size` 3 = triad, 4 = seventh.
|
|
40
|
+
* Because it stacks the actual scale, chord quality (maj/min/dim) falls out of
|
|
41
|
+
* the mode automatically — no quality table to get wrong.
|
|
42
|
+
*/
|
|
43
|
+
export declare function diatonicChord(tonic: number, mode: ScaleName, degree: number, size?: number): number[];
|
|
44
|
+
/**
|
|
45
|
+
* Realize a roman-numeral progression in a key to concrete chords (arrays of
|
|
46
|
+
* MIDI notes). Case-insensitive on the numeral; a trailing "7" adds a seventh.
|
|
47
|
+
* e.g. progression(noteToMidi('C4'),'major',['I','V','vi','IV']).
|
|
48
|
+
*/
|
|
49
|
+
export declare function progression(tonic: number, mode: ScaleName, romans: string[]): number[][];
|
|
50
|
+
/** Transpose a set of MIDI notes by semitones. */
|
|
51
|
+
export declare function transpose(notes: number[], semitones: number): number[];
|
|
52
|
+
/**
|
|
53
|
+
* Open a close-position chord: drop the root an octave so the voicing spreads
|
|
54
|
+
* out and the root/7th no longer clash in the same octave. Turns muddy stacked
|
|
55
|
+
* sevenths into something that breathes.
|
|
56
|
+
*/
|
|
57
|
+
export declare function openVoicing(chord: number[]): number[];
|
|
58
|
+
/**
|
|
59
|
+
* Smooth a progression's voice-leading. Each chord after the first is octave-
|
|
60
|
+
* shifted so its notes sit near the previous chord's register — voices move by
|
|
61
|
+
* small steps instead of leaping in parallel root position. The single biggest
|
|
62
|
+
* cure for the "MIDI demo" sound. Returns new chords (MIDI, sorted low→high).
|
|
63
|
+
*/
|
|
64
|
+
export declare function voiceLead(chords: number[][]): number[][];
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Rng } from '../core/rng';
|
|
2
|
+
import type { Puzzle } from '../verify/solver';
|
|
3
|
+
import type { SolveOptions } from '../verify/solver';
|
|
4
|
+
import { type DifficultyBand, type GeneratedLevel } from './generate';
|
|
5
|
+
/** One named stretch of the campaign, generated inside its own difficulty band. */
|
|
6
|
+
export interface ActSpec<State = unknown, Move = unknown> extends DifficultyBand {
|
|
7
|
+
/** Display name ("The Shallows", "Deep Roots", …). */
|
|
8
|
+
name: string;
|
|
9
|
+
/** How many verified levels this act contributes. */
|
|
10
|
+
count: number;
|
|
11
|
+
/**
|
|
12
|
+
* Optional per-act candidate factory — overrides the campaign default. This is
|
|
13
|
+
* how an act introduces a NEW mechanic or board size (a bigger grid, a new tile
|
|
14
|
+
* kind) while the earlier acts keep the simpler one: mechanic-gating as data.
|
|
15
|
+
*/
|
|
16
|
+
factory?: (rng: Rng) => Puzzle<State, Move>;
|
|
17
|
+
/** Optional per-act solver budget override. */
|
|
18
|
+
solve?: SolveOptions;
|
|
19
|
+
/** Optional per-act attempt cap override. */
|
|
20
|
+
maxAttempts?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface CampaignSpec<State, Move> {
|
|
23
|
+
/** Default candidate factory — used by any act that doesn't bring its own. */
|
|
24
|
+
factory?: (rng: Rng) => Puzzle<State, Move>;
|
|
25
|
+
/** Ordered acts; earlier acts should sit in lower difficulty bands. */
|
|
26
|
+
acts: ActSpec<State, Move>[];
|
|
27
|
+
/** Base seed for the whole campaign (default 1). */
|
|
28
|
+
seed?: number;
|
|
29
|
+
/** Default solver budget for acts that don't override it. */
|
|
30
|
+
solve?: SolveOptions;
|
|
31
|
+
/** Rough authoring estimate: minutes a median player spends per level (default 1.5). */
|
|
32
|
+
minutesPerLevel?: number;
|
|
33
|
+
}
|
|
34
|
+
/** A generated level tagged with the act it belongs to and its campaign index. */
|
|
35
|
+
export interface CampaignLevel<State, Move> extends GeneratedLevel<State, Move> {
|
|
36
|
+
/** Zero-based act index. */
|
|
37
|
+
act: number;
|
|
38
|
+
/** The act's display name. */
|
|
39
|
+
actName: string;
|
|
40
|
+
/** Position within its act. */
|
|
41
|
+
actIndex: number;
|
|
42
|
+
}
|
|
43
|
+
export interface ActBounds {
|
|
44
|
+
name: string;
|
|
45
|
+
/** First campaign index (inclusive). */
|
|
46
|
+
from: number;
|
|
47
|
+
/** Last campaign index (inclusive). */
|
|
48
|
+
to: number;
|
|
49
|
+
}
|
|
50
|
+
export interface Campaign<State, Move> {
|
|
51
|
+
levels: CampaignLevel<State, Move>[];
|
|
52
|
+
acts: ActBounds[];
|
|
53
|
+
/** Per-level difficulty (solver depth), campaign order — feed to `rampIssues`. */
|
|
54
|
+
difficulty: number[];
|
|
55
|
+
/** Reproduce the campaign: every level's sub-seed, in order. */
|
|
56
|
+
seeds: number[];
|
|
57
|
+
/** Honest length estimate in minutes (levels × minutesPerLevel). */
|
|
58
|
+
estMinutes: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Compose a full campaign by generating each act inside its band and concatenating
|
|
62
|
+
* them in order. Deterministic: same spec → same campaign everywhere. Acts are
|
|
63
|
+
* seeded independently (base seed mixed with the act index) so re-tuning one act's
|
|
64
|
+
* band or count never reshuffles the others.
|
|
65
|
+
*
|
|
66
|
+
* Throws (via `generateLevels`) if any act's band is too tight to fill — a loud,
|
|
67
|
+
* early failure beats silently shipping a short act.
|
|
68
|
+
*/
|
|
69
|
+
export declare function composeCampaign<State, Move>(spec: CampaignSpec<State, Move>): Campaign<State, Move>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Rng } from '../core/rng';
|
|
2
|
+
import { type WeightedEntry } from '../logic/random';
|
|
3
|
+
/**
|
|
4
|
+
* One declarative wave. Fires `count` of `spawn` at time `time` (sim seconds).
|
|
5
|
+
* When `every` is set it repeats on that interval until `end` (exclusive),
|
|
6
|
+
* modelling a continuous spawn stream. `spawn` may be a single id or a weighted
|
|
7
|
+
* table, in which case each firing rolls the table via `world.rng`.
|
|
8
|
+
*/
|
|
9
|
+
export interface WaveDef {
|
|
10
|
+
/** First fire time, in fixed sim seconds (compare against `world.time`). */
|
|
11
|
+
time: number;
|
|
12
|
+
/** What to spawn: a fixed id, or a weighted set rolled per firing. */
|
|
13
|
+
spawn: string | WeightedEntry<string>[];
|
|
14
|
+
/** How many per firing (default 1). */
|
|
15
|
+
count?: number;
|
|
16
|
+
/** Repeat interval in seconds; omit for a one-shot wave. */
|
|
17
|
+
every?: number;
|
|
18
|
+
/** Repeats stop once the next fire time reaches this (seconds). */
|
|
19
|
+
end?: number;
|
|
20
|
+
}
|
|
21
|
+
/** Mutable director cursor — plain JSON, keep it in `world.state`. */
|
|
22
|
+
export interface DirectorState {
|
|
23
|
+
/** Next fire time per wave index; Infinity once a wave is exhausted. */
|
|
24
|
+
next: number[];
|
|
25
|
+
}
|
|
26
|
+
/** One resolved spawn instruction emitted by the director. */
|
|
27
|
+
export interface SpawnEvent {
|
|
28
|
+
/** Concrete spawn id (already rolled if the wave used a weighted set). */
|
|
29
|
+
spawn: string;
|
|
30
|
+
count: number;
|
|
31
|
+
/** Source wave index, for callers that want to key behavior off the wave. */
|
|
32
|
+
wave: number;
|
|
33
|
+
}
|
|
34
|
+
/** Initialize a director cursor for a schedule. */
|
|
35
|
+
export declare function initDirector(waves: readonly WaveDef[]): DirectorState;
|
|
36
|
+
/**
|
|
37
|
+
* Advance the director to `time` (sim seconds) and return every spawn that is
|
|
38
|
+
* now due, in wave order. Mutates `state.next` in place. Deterministic: it emits
|
|
39
|
+
* the same events for the same (schedule, time, rng-state) on every machine.
|
|
40
|
+
* A repeating wave can fire multiple times in one poll if the frame straddled
|
|
41
|
+
* several intervals (e.g. after a restore), so catch-up never drops spawns.
|
|
42
|
+
*/
|
|
43
|
+
export declare function pollDirector(waves: readonly WaveDef[], state: DirectorState, time: number, rng?: Rng): SpawnEvent[];
|
|
44
|
+
/**
|
|
45
|
+
* A declarative upgrade or evolution node. `requires` lists ids that must all be
|
|
46
|
+
* owned before this one can be offered — the cat-survivors evolution gate and a
|
|
47
|
+
* deckbuilder's upgrade prerequisites are the same shape.
|
|
48
|
+
*/
|
|
49
|
+
export interface UpgradeDef {
|
|
50
|
+
id: string;
|
|
51
|
+
/** Ids that must all be owned before this becomes available. */
|
|
52
|
+
requires?: string[];
|
|
53
|
+
/** Optional: only offer while UNDER this many owned copies (repeatable upgrades). */
|
|
54
|
+
maxStacks?: number;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Which upgrades are offerable given what's owned: prerequisites all met, and
|
|
58
|
+
* not already at `maxStacks` (default 1 — offered until owned once). `owned` may
|
|
59
|
+
* repeat ids to represent stacks. Ordered by definition order for hash stability.
|
|
60
|
+
*/
|
|
61
|
+
export declare function availableUpgrades(defs: readonly UpgradeDef[], owned: readonly string[]): UpgradeDef[];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Rng } from '../core/rng';
|
|
2
|
+
import { type Puzzle, type SolveOptions } from '../verify/solver';
|
|
3
|
+
/** A difficulty window expressed on the solver's own proof metrics. */
|
|
4
|
+
export interface DifficultyBand {
|
|
5
|
+
/** Minimum solution length (shortest winning move count). Below this = trivial. */
|
|
6
|
+
minDepth?: number;
|
|
7
|
+
/** Maximum solution length. Above this = a slog / likely unfair. */
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
/** Minimum nodes the solver expanded (a branchiness / "how much thinking" proxy). */
|
|
10
|
+
minNodes?: number;
|
|
11
|
+
/** Maximum nodes expanded — caps combinatorial monsters. */
|
|
12
|
+
maxNodes?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface GenerateOptions<State, Move> extends DifficultyBand {
|
|
15
|
+
/** How many verified levels to return. */
|
|
16
|
+
count: number;
|
|
17
|
+
/** Base seed; the whole run is a pure function of it (default 1). */
|
|
18
|
+
seed?: number;
|
|
19
|
+
/** Cap on candidate attempts before giving up (default count * 60). */
|
|
20
|
+
maxAttempts?: number;
|
|
21
|
+
/** Search budget handed to the solver per candidate. */
|
|
22
|
+
solve?: SolveOptions;
|
|
23
|
+
/**
|
|
24
|
+
* Extra rejection hook — return true to discard an otherwise in-band candidate
|
|
25
|
+
* (e.g. a genre-specific "too few boxes" or "spawn adjacent to goal" rule).
|
|
26
|
+
*/
|
|
27
|
+
reject?: (puzzle: Puzzle<State, Move>, result: AcceptedProof<Move>, subSeed: number) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Canonical de-dup key for a candidate. Defaults to the puzzle's own initial
|
|
30
|
+
* state key, so two seeds that happen to build the same layout collapse to one.
|
|
31
|
+
*/
|
|
32
|
+
dedupeKey?: (puzzle: Puzzle<State, Move>, result: AcceptedProof<Move>) => string;
|
|
33
|
+
}
|
|
34
|
+
/** The solver proof carried by an accepted candidate. */
|
|
35
|
+
export interface AcceptedProof<Move> {
|
|
36
|
+
/** Shortest winning move sequence (the existence proof). */
|
|
37
|
+
path: Move[];
|
|
38
|
+
/** Its length — the primary difficulty metric. */
|
|
39
|
+
depth: number;
|
|
40
|
+
/** Nodes the solver expanded to find it — a complexity proxy. */
|
|
41
|
+
nodes: number;
|
|
42
|
+
}
|
|
43
|
+
/** A generated, solver-verified level: reproducible from `seed`, ramped by `depth`. */
|
|
44
|
+
export interface GeneratedLevel<State, Move> extends AcceptedProof<Move> {
|
|
45
|
+
/** Position in the returned (difficulty-sorted) sequence. */
|
|
46
|
+
index: number;
|
|
47
|
+
/** The sub-seed that rebuilds this exact puzzle via `factory(new Rng(seed))`. */
|
|
48
|
+
seed: number;
|
|
49
|
+
/** The built puzzle instance (single-level: `initial()` returns this candidate). */
|
|
50
|
+
puzzle: Puzzle<State, Move>;
|
|
51
|
+
}
|
|
52
|
+
export interface GenerateReport<State, Move> {
|
|
53
|
+
levels: GeneratedLevel<State, Move>[];
|
|
54
|
+
/** Candidates tried. */
|
|
55
|
+
attempts: number;
|
|
56
|
+
/** How many were solvable at all (in-band or not). */
|
|
57
|
+
solvable: number;
|
|
58
|
+
/** True if `count` levels were found before `maxAttempts` ran out. */
|
|
59
|
+
complete: boolean;
|
|
60
|
+
/** Reproduce the whole run: the sub-seeds, in ramp order. */
|
|
61
|
+
seeds: number[];
|
|
62
|
+
}
|
|
63
|
+
/** Derive an independent, reproducible sub-seed for attempt `i` of a base run. */
|
|
64
|
+
export declare function subSeed(seed: number, i: number): number;
|
|
65
|
+
/**
|
|
66
|
+
* Generate `count` solver-verified levels inside a difficulty band. Builds
|
|
67
|
+
* candidates from `factory`, proves each with the BFS solver, and keeps the
|
|
68
|
+
* in-band, non-duplicate ones — sorted by solution depth so the returned
|
|
69
|
+
* sequence already ramps. Pure: same options → same levels, everywhere.
|
|
70
|
+
*
|
|
71
|
+
* A capped solver search (`exhausted`) is treated as "unknown, reject" — a level
|
|
72
|
+
* only ships if it was actually PROVEN winnable within budget.
|
|
73
|
+
*/
|
|
74
|
+
export declare function generateLevelsReport<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, opts: GenerateOptions<State, Move>): GenerateReport<State, Move>;
|
|
75
|
+
/** Convenience: just the ramped levels. Throws if the band was too tight to fill. */
|
|
76
|
+
export declare function generateLevels<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, opts: GenerateOptions<State, Move>): GeneratedLevel<State, Move>[];
|
|
77
|
+
/** Rebuild the exact puzzle for a stored sub-seed — the campaign ships seeds, not maps. */
|
|
78
|
+
export declare function levelFromSeed<State, Move>(factory: (rng: Rng) => Puzzle<State, Move>, seed: number): Puzzle<State, Move>;
|