@algo-wasm/algo-wasm 1.1.0 → 1.2.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +6 -4
  3. package/dist/algo-wasm-error.d.ts +13 -0
  4. package/dist/algo-wasm-player.d.ts +169 -0
  5. package/dist/algo_wasm.wasm +0 -0
  6. package/dist/built-in-profiles.d.ts +5 -0
  7. package/dist/engine-core.d.ts +96 -0
  8. package/dist/events.d.ts +5 -1
  9. package/dist/index.d.ts +3 -4
  10. package/dist/index.js +1 -6
  11. package/dist/integrations/phaser.d.ts +9 -2
  12. package/dist/integrations/phaser.js +1 -16
  13. package/dist/integrations/react.d.ts +8 -2
  14. package/dist/integrations/react.js +1 -28
  15. package/dist/integrations/svelte.d.ts +6 -2
  16. package/dist/integrations/svelte.js +1 -17
  17. package/dist/integrations/vue.d.ts +7 -2
  18. package/dist/integrations/vue.js +1 -16
  19. package/dist/load-wasm.d.ts +92 -0
  20. package/dist/profile-schema.json +317 -228
  21. package/dist/profile-spec.d.ts +178 -0
  22. package/dist/profile-wire.d.ts +50 -0
  23. package/dist/profile.d.ts +25 -1
  24. package/dist/types.d.ts +25 -1
  25. package/dist/wire-decode.d.ts +18 -0
  26. package/dist/worklet/algo-worklet.d.ts +1 -83
  27. package/dist/worklet/algo-worklet.js +1 -471
  28. package/package.json +1 -1
  29. package/dist/AlgoWasmError.d.ts +0 -8
  30. package/dist/AlgoWasmError.d.ts.map +0 -1
  31. package/dist/AlgoWasmError.js +0 -9
  32. package/dist/AlgoWasmError.js.map +0 -1
  33. package/dist/AlgoWasmPlayer.d.ts +0 -48
  34. package/dist/AlgoWasmPlayer.d.ts.map +0 -1
  35. package/dist/AlgoWasmPlayer.js +0 -375
  36. package/dist/AlgoWasmPlayer.js.map +0 -1
  37. package/dist/events.d.ts.map +0 -1
  38. package/dist/events.js +0 -25
  39. package/dist/events.js.map +0 -1
  40. package/dist/index.d.ts.map +0 -1
  41. package/dist/index.js.map +0 -1
  42. package/dist/integrations/phaser.d.ts.map +0 -1
  43. package/dist/integrations/phaser.js.map +0 -1
  44. package/dist/integrations/react.d.ts.map +0 -1
  45. package/dist/integrations/react.js.map +0 -1
  46. package/dist/integrations/svelte.d.ts.map +0 -1
  47. package/dist/integrations/svelte.js.map +0 -1
  48. package/dist/integrations/vue.d.ts.map +0 -1
  49. package/dist/integrations/vue.js.map +0 -1
  50. package/dist/loadWasm.d.ts +0 -47
  51. package/dist/loadWasm.d.ts.map +0 -1
  52. package/dist/loadWasm.js +0 -229
  53. package/dist/loadWasm.js.map +0 -1
  54. package/dist/profile.d.ts.map +0 -1
  55. package/dist/profile.js +0 -525
  56. package/dist/profile.js.map +0 -1
  57. package/dist/types.d.ts.map +0 -1
  58. package/dist/types.js +0 -2
  59. package/dist/types.js.map +0 -1
  60. package/dist/worklet/algo-worklet.d.ts.map +0 -1
  61. package/dist/worklet/algo-worklet.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -6,6 +6,54 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
 
9
+ ## [1.2.0] - 2026-11-13
10
+
11
+ ### Added
12
+
13
+ - A `dubstep` built-in profile: half-time drums, a sustained sine sub bass, sparse mids, and a tempo-locked wobble.
14
+ - A stereo reverb, fed by each patch's `reverbSend`. Earlier releases sent `reverbSend` to the delay.
15
+ - Kick ducking, a second detuned oscillator, a filter envelope, a tempo-synced filter LFO, and stereo width, through the optional patch fields `duck`, `detuneCents`, `filterEnvOctaves`, `filterEnvDecayMs`, `lfoRate`, `lfoDepth`, and `width`.
16
+ - Optional `rhythmRules.halfTimeChance` and `bassRules.sustainChance`.
17
+ - `exportMidi({ downloadName })` now offers the file as a download. The option was accepted but ignored before.
18
+ - `profiles/*.json` as the single source for built-in profiles, with `npm run generate:profiles`.
19
+
20
+ ### Changed
21
+
22
+ - Sound: the master waveshaper is replaced by a look-ahead limiter, the delay is tempo-synced, oscillators are band-limited, and the built-in profiles are rebalanced so chords, lead, and arp are audible. Every seed still composes the same notes as in 1.1.0, but renders are not sample-identical.
23
+ - The WASM binary is 103 KB, down from 347 KB. The JavaScript is bundled and minified, and source maps are no longer published.
24
+ - The engine has no Rust dependencies. `serde` and `serde_json` are removed, and profiles, snapshots, and events cross the WASM boundary as numeric buffers.
25
+ - `renderOffline` applies the player's live volume, energy, intensity, mood, tempo range, mutes, and solos, yields to the page while rendering, and reuses the compiled module.
26
+ - Master volume is applied after the limiter.
27
+ - `AlgoWasmEngineBinding.events()` returns the events since its last call, not only those of the last rendered block.
28
+ - `limits.maxVoices` is honoured. It was validated but ignored before.
29
+ - `limits.maxEventsPerBlock` still limits events per rendered block. The engine now also holds up to 512 events between reads.
30
+
31
+ ### Fixed
32
+
33
+ - About 94% of `beat`, `bar`, and `section-change` events never reached JavaScript. Every event is now delivered.
34
+ - The delay send multiplied the whole mix by a level instead of sending each track's own signal, which distorted the mix.
35
+ - Notes clicked at release, on retrigger, and when a voice was reused. Release times longer than 80 ms were cut short.
36
+ - `AlgoWasmPlayer.create` left a connected node and an open `AudioContext` behind when loading failed.
37
+ - A destroyed player's worklet processor kept running inside a caller-owned `AudioContext`.
38
+ - `setTempoRange(NaN, NaN)` crashed the engine.
39
+ - A profile with a very large `limits.maxEventsPerBlock` could exhaust memory.
40
+ - `setSeed` and `setProfile` silenced a playing song until `start` was called again. The song now restarts from its first bar and keeps playing.
41
+ - Closed hi-hats now cut off a ringing open hi-hat.
42
+ - The one-pole filter used a wrong coefficient, so the effects sweep differed between sample rates.
43
+
44
+ ### Migration
45
+
46
+ No exported name was removed or renamed. These inputs were accepted before and are now rejected:
47
+
48
+ - Unknown scale names. They used to fall back to natural minor without warning. Use `natural_minor`, `dorian`, `aeolian`, `phrygian_dominant`, or `minor_pentatonic`.
49
+ - Out-of-range `chordRules.cadenceWeight`, `chordRules.movementWeight` (0 to 4), `melodyRules.maxRangeSemitones` (1 to 48), `limits.maxEventsPerBlock` (1 to 512), envelope times above 10,000 ms, more than eight scales, scale weights above 1,000,000, `limits.minBpm` or `limits.maxBpm` outside 1 to 1000, and section lengths above 64 bars.
50
+ - `setMood`, `setMuted`, and `setSolo` throw a `TypeError` for an unknown id, and `setTempoRange` throws for values that are not finite and ordered. They used to report through the `error` event.
51
+ - `renderOffline` throws a `RangeError` for `seconds` outside 0 to 600 or an unsupported `sampleRate`.
52
+
53
+ The WASM file and the JavaScript must come from the same release. A mismatch is reported as an error when the engine is created.
54
+
55
+ The Rust crate `algo-wasm-core` is not published, but if you depend on it by path: `Profile::from_json`, `Profile::to_json`, `Engine::snapshot_json`, and `Engine::current_events_json` are removed. Use `Profile::from_wire`, `Snapshot::to_wire`, and `Engine::drain_events`.
56
+
9
57
  ## [1.1.0] - 2026-09-23
10
58
 
11
59
  ### Added
package/README.md CHANGED
@@ -4,13 +4,15 @@ AlgoWASM is a WebAssembly-first deterministic algorithmic music engine for moder
4
4
 
5
5
  It includes:
6
6
 
7
- - `dist/index.js`: ESM TypeScript SDK output.
7
+ - `dist/index.js`: the SDK as one minified ES module.
8
8
  - `dist/index.d.ts`: public TypeScript declarations.
9
9
  - `dist/algo_wasm.wasm`: Rust engine compiled to WebAssembly.
10
- - `dist/worklet/algo-worklet.js`: AudioWorklet processor.
11
- - `dist/profile-schema.json`: JSON schema for profiles.
10
+ - `dist/worklet/algo-worklet.js`: the AudioWorklet processor as one self-contained minified file.
11
+ - `dist/profile-schema.json`: JSON schema for profiles, generated from the validator's own field table.
12
12
 
13
- See the repository README for full integration notes, examples, browser deployment notes, and development commands.
13
+ The package has no runtime dependencies.
14
+
15
+ See the [getting started guide](https://github.com/0xe25f/algowasm/blob/main/docs/getting-started.md) for integration steps, and the [live demo](https://0xe25f.github.io/algowasm/) to hear it.
14
16
 
15
17
  ## Quick Start
16
18
 
@@ -0,0 +1,13 @@
1
+ import type { AlgoWasmErrorCode } from './types.js';
2
+ /**
3
+ * The error type thrown and emitted by AlgoWASM. Check `code` to tell the
4
+ * kinds of failure apart; `cause` holds the underlying error when there is one.
5
+ */
6
+ export declare class AlgoWasmError extends Error {
7
+ /** A stable identifier for the kind of failure. */
8
+ readonly code: AlgoWasmErrorCode;
9
+ /** Creates an error with a stable code, a readable message, and an optional cause. */
10
+ constructor(code: AlgoWasmErrorCode, message: string, options?: {
11
+ cause?: unknown;
12
+ });
13
+ }
@@ -0,0 +1,169 @@
1
+ import type { AlgoWasmEventHandler, AlgoWasmEventName, AlgoWasmGeneratedEvent, AlgoWasmMidiExportOptions, AlgoWasmMood, AlgoWasmOfflineRenderOptions, AlgoWasmPlayerOptions, AlgoWasmProfile, AlgoWasmSnapshot, AlgoWasmTrackId } from './types.js';
2
+ /**
3
+ * Plays generated music through an `AudioWorklet`.
4
+ *
5
+ * Create a player with `AlgoWasmPlayer.create`, call `start` from a user
6
+ * gesture, and call `destroy` when finished. `destroy` frees the engine's
7
+ * WASM memory and, if the player created its own `AudioContext`, closes it.
8
+ *
9
+ * ```ts
10
+ * const player = await AlgoWasmPlayer.create({ wasmUrl, workletUrl, seed: 1995n });
11
+ * button.addEventListener('click', () => player.start());
12
+ * // Later:
13
+ * await player.destroy();
14
+ * ```
15
+ */
16
+ export declare class AlgoWasmPlayer {
17
+ private readonly emitter;
18
+ private readonly audioContext;
19
+ private readonly ownsAudioContext;
20
+ private readonly node;
21
+ private readonly wasmUrl;
22
+ private readonly pending;
23
+ private commandId;
24
+ private destroyed;
25
+ private destroyPromise?;
26
+ private offlineModule?;
27
+ private profile;
28
+ private seed;
29
+ private lastSnapshot;
30
+ private lastEvents;
31
+ private volume;
32
+ private energy;
33
+ private intensity;
34
+ private mood;
35
+ private tempoRange?;
36
+ private readonly muted;
37
+ private readonly solo;
38
+ private constructor();
39
+ /**
40
+ * Loads the worklet and the WASM engine and returns a ready player.
41
+ *
42
+ * No sound plays until `start` is called. If loading fails, everything
43
+ * created along the way is released before the error is thrown.
44
+ *
45
+ * @throws {TypeError} When `wasmUrl` or `workletUrl` is missing.
46
+ * @throws {AlgoWasmError} With code `browser_feature_missing`,
47
+ * `profile_invalid`, `worklet_load_failed`, `wasm_load_failed`, or
48
+ * `engine_failed`.
49
+ */
50
+ static create(options: AlgoWasmPlayerOptions): Promise<AlgoWasmPlayer>;
51
+ /**
52
+ * Starts playback. Call this from a user gesture such as a click, because
53
+ * browsers keep audio locked until then.
54
+ *
55
+ * @throws {AlgoWasmError} With code `audio_context_locked`, `engine_failed`,
56
+ * or `destroyed`.
57
+ */
58
+ start(): Promise<void>;
59
+ /** Pauses playback, keeping the position in the song. */
60
+ pause(): Promise<void>;
61
+ /** Resumes playback after `pause`. */
62
+ resume(): Promise<void>;
63
+ /** Stops playback and rewinds the song to its first bar. */
64
+ stop(): Promise<void>;
65
+ /**
66
+ * Frees the engine and its WASM memory, disconnects the audio node, removes
67
+ * every event handler, and closes the `AudioContext` if the player created
68
+ * it. Safe to call more than once; later calls return the same promise.
69
+ */
70
+ destroy(): Promise<void>;
71
+ private performDestroy;
72
+ /**
73
+ * Changes the seed, which selects a different song. The new song starts
74
+ * from its first bar, and keeps playing if the player was playing.
75
+ *
76
+ * @throws {AlgoWasmError} With code `profile_invalid` for a malformed seed.
77
+ */
78
+ setSeed(seed: bigint | number | string): void;
79
+ /**
80
+ * Changes the profile. The song restarts from its first bar, and keeps
81
+ * playing if the player was playing.
82
+ *
83
+ * @throws {AlgoWasmError} With code `profile_invalid`.
84
+ */
85
+ setProfile(profile: AlgoWasmProfile): void;
86
+ /** Sets master volume. Values are clamped to 0 to 1. */
87
+ setVolume(volume: number): void;
88
+ /**
89
+ * Mutes or unmutes a track.
90
+ *
91
+ * @throws {TypeError} When the track id is not recognised.
92
+ */
93
+ setMuted(trackId: AlgoWasmTrackId, muted: boolean): void;
94
+ /**
95
+ * Solos or unsolos a track. While any track is soloed, only soloed tracks
96
+ * are heard.
97
+ *
98
+ * @throws {TypeError} When the track id is not recognised.
99
+ */
100
+ setSolo(trackId: AlgoWasmTrackId, solo: boolean): void;
101
+ /** Sets energy, which drives arrangement density. Clamped to 0 to 1. */
102
+ setEnergy(value: number): void;
103
+ /** Sets intensity, which drives overall level. Clamped to 0 to 1. */
104
+ setIntensity(value: number): void;
105
+ /**
106
+ * Sets the mood, which tilts the tone brighter or darker.
107
+ *
108
+ * @throws {TypeError} When the mood is not recognised.
109
+ */
110
+ setMood(mood: AlgoWasmMood): void;
111
+ /**
112
+ * Narrows or moves the tempo range. The song keeps its relative place in
113
+ * the range. The range must sit inside the profile's `limits`; if it does
114
+ * not, an `error` event is emitted and the tempo is unchanged.
115
+ *
116
+ * @throws {TypeError} When either value is not a finite number.
117
+ * @throws {RangeError} When `minBpm` is greater than `maxBpm`.
118
+ */
119
+ setTempoRange(minBpm: number, maxBpm: number): void;
120
+ /** Returns a copy of the most recent engine state. */
121
+ getSnapshot(): AlgoWasmSnapshot;
122
+ /** Returns a copy of the most recent batch of generated events. */
123
+ getCurrentEvents(): AlgoWasmGeneratedEvent[];
124
+ /**
125
+ * Exports the current four-bar phrase as a type-0 MIDI file.
126
+ *
127
+ * When `options.downloadName` is given and a document is available, the
128
+ * file is also offered to the user as a download with that name.
129
+ */
130
+ exportMidi(options?: AlgoWasmMidiExportOptions): Promise<Uint8Array>;
131
+ /**
132
+ * Renders the song from its first bar into interleaved stereo samples
133
+ * (left, right, left, ...), without playing it.
134
+ *
135
+ * The render uses the player's current volume, energy, intensity, mood,
136
+ * tempo range, mutes, and solos. It runs in chunks and yields to the page
137
+ * between them, so long renders do not freeze the tab.
138
+ *
139
+ * @throws {RangeError} When `seconds` is not between 0 and 600, or
140
+ * `sampleRate` is not a whole number from 22050 to 96000.
141
+ * @throws {AlgoWasmError} With code `profile_invalid`, `wasm_load_failed`,
142
+ * `engine_failed`, or `destroyed`.
143
+ */
144
+ renderOffline(options: AlgoWasmOfflineRenderOptions): Promise<Float32Array>;
145
+ /**
146
+ * Subscribes to a player event and returns a function that unsubscribes.
147
+ *
148
+ * `beat`, `bar`, and `section-change` arrive in batches about every 21 ms,
149
+ * so they can trail the audio by that much.
150
+ */
151
+ on<Name extends AlgoWasmEventName>(eventName: Name, handler: AlgoWasmEventHandler<Name>): () => void;
152
+ /** Removes a handler added with `on`. */
153
+ off<Name extends AlgoWasmEventName>(eventName: Name, handler: AlgoWasmEventHandler<Name>): void;
154
+ /**
155
+ * Compiles the WASM module for offline rendering on first use and reuses
156
+ * it afterwards, so repeated renders do not fetch or compile again.
157
+ */
158
+ private compileOffline;
159
+ private configure;
160
+ /**
161
+ * Sends a command without waiting for it. A failure is reported through
162
+ * the `error` event. `onAccepted` runs only if the engine accepts it.
163
+ */
164
+ private postLiveCommand;
165
+ private sendBestEffort;
166
+ private sendCommand;
167
+ private handleMessage;
168
+ private assertAlive;
169
+ }
Binary file
@@ -0,0 +1,5 @@
1
+ import type { AlgoWasmProfile } from './types.js';
2
+ /** Identifiers of the built-in profiles, as used by `builtInProfiles`. */
3
+ export type BuiltInProfileName = 'amigaHouse95ish' | 'downtempoBreakbeat' | 'dubDeepHouse' | 'dubstep';
4
+ /** The built-in profiles. Clone before use; `builtInProfiles` does this for you. */
5
+ export declare const builtInProfileData: Record<BuiltInProfileName, AlgoWasmProfile>;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Low-level, numbers-only binding to one engine inside a WASM instance.
3
+ *
4
+ * Both the audio worklet and the main thread use this class, so the boundary
5
+ * code exists once. Nothing here allocates per call once the buffers exist,
6
+ * apart from the copies returned by `copySnapshot`, `copyEvents`, and
7
+ * `exportMidi`, which makes it safe to use on the audio thread.
8
+ *
9
+ * Memory ownership: the binding owns every WASM buffer it allocates and
10
+ * frees them in `destroy`. Call `destroy` when finished.
11
+ */
12
+ export declare class EngineCore {
13
+ private readonly exports;
14
+ private enginePtr;
15
+ private renderPtr;
16
+ private renderLength;
17
+ private renderView?;
18
+ private failed;
19
+ /**
20
+ * @throws {AlgoWasmError} With code `wasm_load_failed` when the instance
21
+ * does not export memory, which means it is not an AlgoWASM binary.
22
+ */
23
+ constructor(instance: WebAssembly.Instance);
24
+ /**
25
+ * Creates the engine from a profile in the flat layout written by
26
+ * `encodeProfile`. Any existing engine is destroyed first.
27
+ *
28
+ * @throws {AlgoWasmError} With code `engine_failed` and the engine's own
29
+ * message when the profile or sample rate is rejected.
30
+ */
31
+ createFromWire(sampleRate: number, seedHigh: number, seedLow: number, profile: Float32Array): void;
32
+ /** Starts playback. */
33
+ start(): void;
34
+ /** Pauses playback, keeping the transport position. */
35
+ pause(): void;
36
+ /** Resumes playback after a pause. */
37
+ resume(): void;
38
+ /** Stops playback, rewinds to the first bar, and clears every voice. */
39
+ stop(): void;
40
+ /**
41
+ * Re-seeds the engine from the two halves of a 64-bit seed. The song
42
+ * restarts from its first bar and keeps its play state.
43
+ */
44
+ setSeedParts(seedHigh: number, seedLow: number): void;
45
+ /**
46
+ * Replaces the profile, given in the flat layout. The song restarts from
47
+ * its first bar and keeps its play state.
48
+ */
49
+ setProfileWire(profile: Float32Array): void;
50
+ /** Sets master volume from 0 to 1. */
51
+ setVolume(value: number): void;
52
+ /** Sets energy from 0 to 1. */
53
+ setEnergy(value: number): void;
54
+ /** Sets intensity from 0 to 1. */
55
+ setIntensity(value: number): void;
56
+ /** Sets mood by its index in `WIRE_MOODS`. */
57
+ setMoodIndex(mood: number): void;
58
+ /** Sets the tempo range in beats per minute. */
59
+ setTempoRange(minBpm: number, maxBpm: number): void;
60
+ /** Mutes or unmutes a track by its index in `WIRE_TRACK_IDS`. */
61
+ setMutedIndex(track: number, muted: boolean): void;
62
+ /** Solos or unsolos a track by its index in `WIRE_TRACK_IDS`. */
63
+ setSoloIndex(track: number, solo: boolean): void;
64
+ /** Renders interleaved stereo samples (left, right, left, ...) into `output`. */
65
+ renderTo(output: Float32Array): void;
66
+ /**
67
+ * Renders straight into separate left and right channel buffers, as an
68
+ * audio worklet receives them. The two must be the same length.
69
+ */
70
+ renderPlanar(left: Float32Array, right: Float32Array): void;
71
+ /** Returns a copy of the engine's binary snapshot. See `decodeSnapshot`. */
72
+ copySnapshot(): Float64Array;
73
+ /**
74
+ * Removes the events buffered since the last call and returns a copy of
75
+ * them in binary form, or `undefined` when there were none. See
76
+ * `decodeEvents`.
77
+ */
78
+ copyEvents(): Float64Array | undefined;
79
+ /** Returns the current four-bar phrase as the bytes of a type-0 MIDI file. */
80
+ exportMidi(): Uint8Array;
81
+ /**
82
+ * Frees the engine and every WASM buffer this binding allocated. Safe to
83
+ * call more than once. After an internal engine error nothing is freed,
84
+ * because the instance can no longer be trusted; drop the instance instead.
85
+ */
86
+ destroy(): void;
87
+ private get memory();
88
+ /** Renders `length` interleaved samples and returns a view of them. */
89
+ private render;
90
+ private writeProfile;
91
+ private callEngine;
92
+ private assertCode;
93
+ private ensureEngine;
94
+ private call;
95
+ private lastError;
96
+ }
package/dist/events.d.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import type { AlgoWasmEventHandler, AlgoWasmEventName, AlgoWasmEventPayloads } from './types.js';
2
+ /** A small typed event emitter used by `AlgoWasmPlayer`. */
2
3
  export declare class AlgoWasmEventEmitter {
3
4
  private readonly handlers;
5
+ /** Adds a handler and returns a function that removes it again. */
4
6
  on<Name extends AlgoWasmEventName>(eventName: Name, handler: AlgoWasmEventHandler<Name>): () => void;
7
+ /** Removes a handler. Removing one that was never added does nothing. */
5
8
  off<Name extends AlgoWasmEventName>(eventName: Name, handler: AlgoWasmEventHandler<Name>): void;
9
+ /** Calls every handler for an event, in the order they were added. */
6
10
  emit<Name extends AlgoWasmEventName>(eventName: Name, payload: AlgoWasmEventPayloads[Name]): void;
11
+ /** Removes every handler for every event. */
7
12
  clear(): void;
8
13
  }
9
- //# sourceMappingURL=events.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,7 +1,6 @@
1
- export { AlgoWasmError } from './AlgoWasmError.js';
2
- export { AlgoWasmPlayer } from './AlgoWasmPlayer.js';
1
+ export { AlgoWasmError } from './algo-wasm-error.js';
2
+ export { AlgoWasmPlayer } from './algo-wasm-player.js';
3
3
  export { AlgoWasmEventEmitter } from './events.js';
4
- export { AlgoWasmEngineBinding, instantiateAlgoWasm } from './loadWasm.js';
4
+ export { AlgoWasmEngineBinding, instantiateAlgoWasm } from './load-wasm.js';
5
5
  export { builtInProfiles, cloneProfile, normaliseSeed, splitSeed, trackIds, validateProfile } from './profile.js';
6
6
  export type { AlgoWasmArrangementRules, AlgoWasmAutomationRules, AlgoWasmBassRules, AlgoWasmChordRules, AlgoWasmErrorCode, AlgoWasmEventHandler, AlgoWasmEventName, AlgoWasmEventPayloads, AlgoWasmGeneratedEvent, AlgoWasmMelodyRules, AlgoWasmMetrics, AlgoWasmMidiExportOptions, AlgoWasmMixerProfile, AlgoWasmMixerTrackProfile, AlgoWasmMood, AlgoWasmOfflineRenderOptions, AlgoWasmPatch, AlgoWasmPlayerOptions, AlgoWasmProfile, AlgoWasmRhythmRules, AlgoWasmSafetyLimits, AlgoWasmScaleRule, AlgoWasmSnapshot, AlgoWasmTempoRules, AlgoWasmTrackId, AlgoWasmTrackSnapshot, AlgoWasmWaveShape } from './types.js';
7
- //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1 @@
1
- export { AlgoWasmError } from './AlgoWasmError.js';
2
- export { AlgoWasmPlayer } from './AlgoWasmPlayer.js';
3
- export { AlgoWasmEventEmitter } from './events.js';
4
- export { AlgoWasmEngineBinding, instantiateAlgoWasm } from './loadWasm.js';
5
- export { builtInProfiles, cloneProfile, normaliseSeed, splitSeed, trackIds, validateProfile } from './profile.js';
6
- //# sourceMappingURL=index.js.map
1
+ var e=class extends Error{code;constructor(e,t,n){super(t,n),this.name=`AlgoWasmError`,this.code=e}},t=class{handlers=new Map;on(e,t){let n=this.handlers.get(e)??new Set;return n.add(t),this.handlers.set(e,n),()=>this.off(e,t)}off(e,t){this.handlers.get(e)?.delete(t)}emit(e,t){let n=this.handlers.get(e);if(n)for(let e of n)e(t)}clear(){this.handlers.clear()}},n=[`kick`,`snare`,`closed_hat`,`open_hat`,`percussion`,`bass`,`chords`,`pad`,`lead`,`arp`,`fx`],r=[`sine`,`triangle`,`saw`,`square`,`pulse`,`noise`],i=[`natural_minor`,`dorian`,`aeolian`,`phrygian_dominant`,`minor_pentatonic`],a=[`dark`,`bright`,`tense`,`calm`],o=[`intro`,`groove_a`,`groove_b`,`build`,`drop`,`variation`,`breakdown`,`fill`,`transition`,`reset`],s={halfTimeChance:.3,sustainChance:0,detuneCents:0,filterEnvOctaves:0,filterEnvDecayMs:120,lfoRate:0,lfoDepth:0,width:0,duck:0},c=58+n.length*20;function l(e){let t=new Float32Array(c),a=0,o=(...e)=>{for(let n of e)t[a]=n,a+=1},{tempo:l,chordRules:u,rhythmRules:d,bassRules:f,melodyRules:p}=e,{arrangementRules:m,automationRules:h,mixer:g,limits:_}=e;o(1),o(l.minBpm,l.maxBpm,l.defaultBpm),o(u.cadenceWeight,u.movementWeight,u.suspendedChance,u.brightBorrowChance),o(d.kickDensity,d.hatDensity,d.percussionDensity,d.fillChance,d.halfTimeChance??s.halfTimeChance),o(f.density,f.slideChance,f.accentChance,f.octaveJumpChance,f.sustainChance??s.sustainChance),o(p.density,p.mutationChance,p.callResponseChance,p.maxRangeSemitones),o(m.minSectionBars,m.maxSectionBars,m.futureBars,m.resetChance),o(h.filterMotion,h.sendMotion,h.panMotion),o(g.masterGain,g.limiterDrive,g.delayFeedback,g.reverbMix),o(_.minSampleRate,_.maxSampleRate,_.minBpm,_.maxBpm,_.maxTracks,_.maxVoices,_.maxBarsQueued,_.maxEventsPerBlock),o(e.scales.length);for(let t=0;t<8;t+=1){let n=e.scales[t];o(n?i.indexOf(n.name):0),o(n?n.weight:0)}for(let t of n){let n=e.patches.find(e=>e.track===t);o(r.indexOf(n.wave),n.gain,n.pan,n.attackMs,n.decayMs,n.sustain,n.releaseMs,n.filterCutoff,n.resonance,n.delaySend,n.reverbSend,n.detuneCents??s.detuneCents,n.filterEnvOctaves??s.filterEnvOctaves,n.filterEnvDecayMs??s.filterEnvDecayMs,n.lfoRate??s.lfoRate,n.lfoDepth??s.lfoDepth,n.width??s.width,n.duck??s.duck)}for(let e of n){let t=g.tracks.find(t=>t.track===e);o(t?.gain??1,t?.pan??0)}return t}var u=10+n.length*3;function d(e){return{seed:(BigInt(e[0]??0)<<32n|BigInt(e[1]??0)).toString(),sampleRate:e[2]??0,bpm:e[3]??0,currentBar:e[4]??0,currentBeat:e[5]??1,currentSection:o[e[6]??0]??`intro`,energy:e[7]??0,intensity:e[8]??0,isPlaying:e[9]===1,tracks:n.map((t,n)=>({id:t,muted:e[10+n*3]===1,solo:e[11+n*3]===1,level:e[12+n*3]??0}))}}function f(e){let t=[];for(let r=0;r+5<=e.length;r+=5){let i=e[r+1]??0,a=e[r+2]??0,s=e[r+3]??0,c=e[r+4]??0;switch(e[r]){case 0:t.push({type:`note-on`,sample:i,track:n[a]??`kick`,note:s,velocity:c});break;case 3:t.push({type:`section-change`,sample:i,section:o[a]??`intro`,bar:s});break;case 4:t.push({type:`bar`,sample:i,bar:a});break;case 5:t.push({type:`beat`,sample:i,bar:a,beat:s})}}return t}var p=class{exports;enginePtr=0;renderPtr=0;renderLength=0;renderView;failed=!1;constructor(t){if(this.exports=t.exports,!(this.exports.memory instanceof WebAssembly.Memory))throw new e(`wasm_load_failed`,`AlgoWASM did not export a WebAssembly memory.`)}createFromWire(t,n,r,i){this.destroy();let a=this.writeProfile(i);try{let o=this.call(`algo_wasm_engine_create`,t,n,r,a,i.length);if(o===0)throw new e(`engine_failed`,this.lastError(`AlgoWASM engine creation failed.`));this.enginePtr=o}finally{this.call(`algo_wasm_dealloc`,a,i.byteLength)}}start(){this.callEngine(`algo_wasm_engine_start`)}pause(){this.callEngine(`algo_wasm_engine_pause`)}resume(){this.callEngine(`algo_wasm_engine_resume`)}stop(){this.callEngine(`algo_wasm_engine_stop`)}setSeedParts(e,t){this.callEngine(`algo_wasm_engine_set_seed`,e,t)}setProfileWire(e){this.ensureEngine();let t=this.writeProfile(e);try{this.assertCode(this.call(`algo_wasm_engine_set_profile`,this.enginePtr,t,e.length),`Profile update failed.`)}finally{this.call(`algo_wasm_dealloc`,t,e.byteLength)}}setVolume(e){this.callEngine(`algo_wasm_engine_set_volume`,e)}setEnergy(e){this.callEngine(`algo_wasm_engine_set_energy`,e)}setIntensity(e){this.callEngine(`algo_wasm_engine_set_intensity`,e)}setMoodIndex(e){this.callEngine(`algo_wasm_engine_set_mood`,e)}setTempoRange(e,t){this.callEngine(`algo_wasm_engine_set_tempo_range`,e,t)}setMutedIndex(e,t){this.callEngine(`algo_wasm_engine_set_muted`,e,+!!t)}setSoloIndex(e,t){this.callEngine(`algo_wasm_engine_set_solo`,e,+!!t)}renderTo(e){e.set(this.render(e.length).subarray(0,e.length))}renderPlanar(e,t){let n=this.render(e.length*2);for(let r=0;r<e.length;r+=1)e[r]=n[r*2]??0,t[r]=n[r*2+1]??0}copySnapshot(){this.ensureEngine();let t=this.call(`algo_wasm_engine_snapshot`,this.enginePtr);if(t===0)throw new e(`engine_failed`,this.lastError(`Snapshot failed.`));return new Float64Array(this.memory.buffer,t,u).slice()}copyEvents(){this.ensureEngine();let e=this.call(`algo_wasm_engine_drain_events`,this.enginePtr);if(e===0)return;let t=this.call(`algo_wasm_engine_events_ptr`,this.enginePtr);return new Float64Array(this.memory.buffer,t,e*5).slice()}exportMidi(){this.ensureEngine();let e=this.call(`algo_wasm_engine_export_midi`,this.enginePtr),t=this.call(`algo_wasm_engine_midi_ptr`,this.enginePtr);return new Uint8Array(this.memory.buffer,t,e).slice()}destroy(){this.failed?(this.enginePtr=0,this.renderPtr=0):(this.renderPtr!==0&&(this.call(`algo_wasm_dealloc`,this.renderPtr,this.renderLength*4),this.renderPtr=0,this.renderLength=0,this.renderView=void 0),this.enginePtr!==0&&(this.call(`algo_wasm_engine_destroy`,this.enginePtr),this.enginePtr=0))}get memory(){return this.exports.memory}render(t){if(this.ensureEngine(),this.renderLength<t&&(this.renderPtr!==0&&this.call(`algo_wasm_dealloc`,this.renderPtr,this.renderLength*4),this.renderPtr=this.call(`algo_wasm_alloc`,t*4),this.renderLength=this.renderPtr===0?0:t,this.renderView=void 0,this.renderPtr===0))throw new e(`render_failed`,`Could not allocate the WASM render buffer.`);return this.assertCode(this.call(`algo_wasm_engine_render`,this.enginePtr,this.renderPtr,t),`Render failed.`),this.renderView&&this.renderView.buffer===this.memory.buffer||(this.renderView=new Float32Array(this.memory.buffer,this.renderPtr,this.renderLength)),this.renderView}writeProfile(t){let n=this.call(`algo_wasm_alloc`,t.byteLength);if(n===0)throw new e(`engine_failed`,`Could not allocate a WASM buffer for the profile.`);return new Float32Array(this.memory.buffer,n,t.length).set(t),n}callEngine(e,...t){this.ensureEngine(),this.assertCode(this.call(e,this.enginePtr,...t),`${e} failed.`)}assertCode(t,n){if(t!==0)throw new e(`engine_failed`,this.lastError(n))}ensureEngine(){if(this.enginePtr===0)throw new e(`engine_failed`,`AlgoWASM engine has not been created.`)}call(t,...n){if(this.failed)throw new e(`engine_failed`,`The AlgoWASM engine stopped after an internal error. Create a new player to continue.`);let r=this.exports[t];if(typeof r!=`function`)throw new e(`wasm_load_failed`,`AlgoWASM is missing the ${t} export.`);try{return r(...n)}catch(t){throw this.failed=!0,new e(`engine_failed`,`The AlgoWASM engine stopped after an internal error. Create a new player to continue.`,{cause:t})}}lastError(e){let t=this.call(`algo_wasm_last_error_len`);if(t===0)return e;let n=this.call(`algo_wasm_last_error_ptr`);return String.fromCharCode(...new Uint8Array(this.memory.buffer,n,t))}},m={amigaHouse95ish:{id:`amiga_house_95ish`,displayName:`Amiga House 95ish`,version:`1.2.0`,tempo:{minBpm:118,maxBpm:138,defaultBpm:126},scales:[{name:`natural_minor`,weight:34},{name:`dorian`,weight:24},{name:`aeolian`,weight:20},{name:`phrygian_dominant`,weight:10},{name:`minor_pentatonic`,weight:12}],chordRules:{cadenceWeight:.75,movementWeight:.6,suspendedChance:.24,brightBorrowChance:.08},rhythmRules:{kickDensity:1,hatDensity:.72,percussionDensity:.28,fillChance:.32},bassRules:{density:.62,slideChance:.18,accentChance:.34,octaveJumpChance:.16},melodyRules:{density:.32,mutationChance:.22,callResponseChance:.55,maxRangeSemitones:14},arrangementRules:{minSectionBars:4,maxSectionBars:8,futureBars:8,resetChance:.08},automationRules:{filterMotion:.46,sendMotion:.24,panMotion:.18},patches:[{track:`kick`,wave:`sine`,gain:1,pan:0,attackMs:0,decayMs:120,sustain:0,releaseMs:10,filterCutoff:120,resonance:.1,delaySend:0,reverbSend:0},{track:`snare`,wave:`noise`,gain:.75,pan:0,attackMs:0,decayMs:90,sustain:0,releaseMs:30,filterCutoff:5e3,resonance:.2,delaySend:.05,reverbSend:.08},{track:`closed_hat`,wave:`noise`,gain:.42,pan:.15,attackMs:0,decayMs:35,sustain:0,releaseMs:10,filterCutoff:9e3,resonance:.2,delaySend:.04,reverbSend:.05,width:.5},{track:`open_hat`,wave:`noise`,gain:.32,pan:.25,attackMs:0,decayMs:240,sustain:0,releaseMs:50,filterCutoff:8500,resonance:.2,delaySend:.08,reverbSend:.08,width:.5},{track:`percussion`,wave:`triangle`,gain:.34,pan:-.2,attackMs:1,decayMs:80,sustain:0,releaseMs:20,filterCutoff:2800,resonance:.3,delaySend:.06,reverbSend:.08,width:.7},{track:`bass`,wave:`saw`,gain:.72,pan:0,attackMs:4,decayMs:90,sustain:.35,releaseMs:70,filterCutoff:490,resonance:.62,delaySend:.08,reverbSend:.04,filterEnvOctaves:1.4,filterEnvDecayMs:140,duck:.5},{track:`chords`,wave:`pulse`,gain:.38,pan:-.15,attackMs:5,decayMs:120,sustain:.18,releaseMs:120,filterCutoff:1440,resonance:.36,delaySend:.16,reverbSend:.18,detuneCents:9,filterEnvOctaves:.9,filterEnvDecayMs:180,width:.7,duck:.25},{track:`pad`,wave:`triangle`,gain:.22,pan:0,attackMs:300,decayMs:700,sustain:.62,releaseMs:600,filterCutoff:1920,resonance:.22,delaySend:.18,reverbSend:.32,detuneCents:12,width:.8,duck:.3},{track:`lead`,wave:`square`,gain:.36,pan:.1,attackMs:8,decayMs:130,sustain:.24,releaseMs:90,filterCutoff:2380,resonance:.28,delaySend:.22,reverbSend:.14,detuneCents:6,filterEnvOctaves:.7,filterEnvDecayMs:220,duck:.15},{track:`arp`,wave:`pulse`,gain:.34,pan:.18,attackMs:2,decayMs:80,sustain:.18,releaseMs:50,filterCutoff:2890,resonance:.24,delaySend:.16,reverbSend:.12,width:.9,duck:.15},{track:`fx`,wave:`noise`,gain:.81,pan:0,attackMs:0,decayMs:600,sustain:0,releaseMs:120,filterCutoff:5500,resonance:.2,delaySend:.2,reverbSend:.26}],mixer:{masterGain:.6,limiterDrive:.96,delayFeedback:.34,reverbMix:.16,tracks:[{track:`kick`,gain:1,pan:0},{track:`snare`,gain:1.95,pan:0},{track:`closed_hat`,gain:1.41,pan:.18},{track:`open_hat`,gain:1.22,pan:.28},{track:`percussion`,gain:1.69,pan:-.24},{track:`bass`,gain:.94,pan:0},{track:`chords`,gain:1.7,pan:-.12},{track:`pad`,gain:1.49,pan:0},{track:`lead`,gain:1.69,pan:.08},{track:`arp`,gain:2,pan:.2},{track:`fx`,gain:2,pan:0}]},limits:{minSampleRate:22050,maxSampleRate:96e3,minBpm:90,maxBpm:160,maxTracks:11,maxVoices:48,maxBarsQueued:16,maxEventsPerBlock:128}},downtempoBreakbeat:{id:`downtempo_breakbeat`,displayName:`Downtempo Breakbeat`,version:`1.2.0`,tempo:{minBpm:88,maxBpm:112,defaultBpm:98},scales:[{name:`natural_minor`,weight:26},{name:`dorian`,weight:30},{name:`aeolian`,weight:16},{name:`phrygian_dominant`,weight:16},{name:`minor_pentatonic`,weight:12}],chordRules:{cadenceWeight:.7,movementWeight:.5,suspendedChance:.3,brightBorrowChance:.05},rhythmRules:{kickDensity:.8,hatDensity:.6,percussionDensity:.5,fillChance:.55},bassRules:{density:.55,slideChance:.14,accentChance:.4,octaveJumpChance:.12},melodyRules:{density:.26,mutationChance:.22,callResponseChance:.5,maxRangeSemitones:12},arrangementRules:{minSectionBars:4,maxSectionBars:8,futureBars:8,resetChance:.07},automationRules:{filterMotion:.5,sendMotion:.3,panMotion:.16},patches:[{track:`kick`,wave:`sine`,gain:.95,pan:0,attackMs:0,decayMs:130,sustain:0,releaseMs:130,filterCutoff:100,resonance:.1,delaySend:0,reverbSend:.05},{track:`snare`,wave:`noise`,gain:.76,pan:0,attackMs:0,decayMs:100,sustain:0,releaseMs:40,filterCutoff:4200,resonance:.2,delaySend:.1,reverbSend:.18},{track:`closed_hat`,wave:`noise`,gain:.36,pan:.12,attackMs:0,decayMs:30,sustain:0,releaseMs:8,filterCutoff:8e3,resonance:.2,delaySend:.05,reverbSend:.1,width:.5},{track:`open_hat`,wave:`noise`,gain:.3,pan:.22,attackMs:0,decayMs:220,sustain:0,releaseMs:45,filterCutoff:7500,resonance:.2,delaySend:.1,reverbSend:.14,width:.5},{track:`percussion`,wave:`triangle`,gain:.4,pan:-.22,attackMs:1,decayMs:90,sustain:0,releaseMs:25,filterCutoff:2400,resonance:.3,delaySend:.12,reverbSend:.16,width:.7},{track:`bass`,wave:`sine`,gain:.75,pan:0,attackMs:6,decayMs:110,sustain:.45,releaseMs:90,filterCutoff:374,resonance:.3,delaySend:.06,reverbSend:.08,filterEnvOctaves:1.4,filterEnvDecayMs:140,duck:.5},{track:`chords`,wave:`triangle`,gain:.34,pan:-.14,attackMs:20,decayMs:160,sustain:.3,releaseMs:220,filterCutoff:1200,resonance:.28,delaySend:.22,reverbSend:.3,detuneCents:9,filterEnvOctaves:.9,filterEnvDecayMs:180,width:.7,duck:.25},{track:`pad`,wave:`triangle`,gain:.26,pan:0,attackMs:400,decayMs:800,sustain:.65,releaseMs:900,filterCutoff:1440,resonance:.2,delaySend:.24,reverbSend:.42,detuneCents:12,width:.8,duck:.3},{track:`lead`,wave:`triangle`,gain:.37,pan:.1,attackMs:14,decayMs:140,sustain:.22,releaseMs:160,filterCutoff:1870,resonance:.24,delaySend:.28,reverbSend:.22,detuneCents:6,filterEnvOctaves:.7,filterEnvDecayMs:220,duck:.15},{track:`arp`,wave:`pulse`,gain:.32,pan:.18,attackMs:3,decayMs:70,sustain:.16,releaseMs:55,filterCutoff:2550,resonance:.22,delaySend:.2,reverbSend:.16,width:.9,duck:.15},{track:`fx`,wave:`noise`,gain:.65,pan:0,attackMs:0,decayMs:700,sustain:0,releaseMs:150,filterCutoff:5e3,resonance:.2,delaySend:.26,reverbSend:.34}],mixer:{masterGain:.7,limiterDrive:.9,delayFeedback:.4,reverbMix:.3,tracks:[{track:`kick`,gain:1,pan:0},{track:`snare`,gain:2,pan:0},{track:`closed_hat`,gain:1.54,pan:.18},{track:`open_hat`,gain:1.06,pan:.28},{track:`percussion`,gain:1,pan:-.24},{track:`bass`,gain:.65,pan:0},{track:`chords`,gain:1.82,pan:-.12},{track:`pad`,gain:1.18,pan:0},{track:`lead`,gain:2,pan:.08},{track:`arp`,gain:2,pan:.2},{track:`fx`,gain:2,pan:0}]},limits:{minSampleRate:22050,maxSampleRate:96e3,minBpm:70,maxBpm:130,maxTracks:11,maxVoices:48,maxBarsQueued:16,maxEventsPerBlock:128}},dubDeepHouse:{id:`dub_deep_house`,displayName:`Dub Deep House`,version:`1.2.0`,tempo:{minBpm:118,maxBpm:124,defaultBpm:121},scales:[{name:`natural_minor`,weight:30},{name:`dorian`,weight:30},{name:`aeolian`,weight:24},{name:`phrygian_dominant`,weight:6},{name:`minor_pentatonic`,weight:10}],chordRules:{cadenceWeight:.8,movementWeight:.4,suspendedChance:.3,brightBorrowChance:.1},rhythmRules:{kickDensity:1,hatDensity:.5,percussionDensity:.22,fillChance:.18},bassRules:{density:.48,slideChance:.05,accentChance:.22,octaveJumpChance:.05},melodyRules:{density:.2,mutationChance:.18,callResponseChance:.4,maxRangeSemitones:10},arrangementRules:{minSectionBars:8,maxSectionBars:16,futureBars:12,resetChance:.05},automationRules:{filterMotion:.6,sendMotion:.42,panMotion:.24},patches:[{track:`kick`,wave:`sine`,gain:1,pan:0,attackMs:0,decayMs:130,sustain:0,releaseMs:140,filterCutoff:110,resonance:.1,delaySend:0,reverbSend:.06},{track:`snare`,wave:`noise`,gain:.63,pan:0,attackMs:0,decayMs:80,sustain:0,releaseMs:30,filterCutoff:4500,resonance:.2,delaySend:.08,reverbSend:.22},{track:`closed_hat`,wave:`noise`,gain:.4,pan:.16,attackMs:0,decayMs:28,sustain:0,releaseMs:8,filterCutoff:8200,resonance:.2,delaySend:.05,reverbSend:.12,width:.5},{track:`open_hat`,wave:`noise`,gain:.3,pan:.26,attackMs:0,decayMs:200,sustain:0,releaseMs:40,filterCutoff:7800,resonance:.2,delaySend:.1,reverbSend:.16,width:.5},{track:`percussion`,wave:`triangle`,gain:.3,pan:-.2,attackMs:1,decayMs:70,sustain:0,releaseMs:20,filterCutoff:2600,resonance:.28,delaySend:.1,reverbSend:.14,width:.7},{track:`bass`,wave:`triangle`,gain:.8,pan:0,attackMs:8,decayMs:120,sustain:.55,releaseMs:110,filterCutoff:302,resonance:.2,delaySend:.04,reverbSend:.06,filterEnvOctaves:1.4,filterEnvDecayMs:140,duck:.5},{track:`chords`,wave:`triangle`,gain:.3,pan:-.1,attackMs:40,decayMs:200,sustain:.4,releaseMs:380,filterCutoff:960,resonance:.24,delaySend:.3,reverbSend:.38,detuneCents:9,filterEnvOctaves:.9,filterEnvDecayMs:180,width:.7,duck:.25},{track:`pad`,wave:`triangle`,gain:.28,pan:0,attackMs:600,decayMs:900,sustain:.7,releaseMs:1100,filterCutoff:1280,resonance:.18,delaySend:.26,reverbSend:.46,detuneCents:12,width:.8,duck:.3},{track:`lead`,wave:`triangle`,gain:.38,pan:.06,attackMs:20,decayMs:150,sustain:.2,releaseMs:200,filterCutoff:1615,resonance:.2,delaySend:.32,reverbSend:.3,detuneCents:6,filterEnvOctaves:.7,filterEnvDecayMs:220,duck:.15},{track:`arp`,wave:`triangle`,gain:.31,pan:.16,attackMs:4,decayMs:80,sustain:.14,releaseMs:70,filterCutoff:2210,resonance:.2,delaySend:.26,reverbSend:.22,width:.9,duck:.15},{track:`fx`,wave:`noise`,gain:.66,pan:0,attackMs:0,decayMs:800,sustain:0,releaseMs:180,filterCutoff:4800,resonance:.2,delaySend:.3,reverbSend:.4}],mixer:{masterGain:.7,limiterDrive:.85,delayFeedback:.46,reverbMix:.36,tracks:[{track:`kick`,gain:1,pan:0},{track:`snare`,gain:2,pan:0},{track:`closed_hat`,gain:1.53,pan:.18},{track:`open_hat`,gain:1.28,pan:.28},{track:`percussion`,gain:1.74,pan:-.24},{track:`bass`,gain:.79,pan:0},{track:`chords`,gain:1.2,pan:-.12},{track:`pad`,gain:1.32,pan:0},{track:`lead`,gain:2,pan:.08},{track:`arp`,gain:2,pan:.2},{track:`fx`,gain:2,pan:0}]},limits:{minSampleRate:22050,maxSampleRate:96e3,minBpm:100,maxBpm:130,maxTracks:11,maxVoices:48,maxBarsQueued:24,maxEventsPerBlock:128}},dubstep:{id:`dubstep`,displayName:`Dubstep`,version:`1.2.0`,tempo:{minBpm:138,maxBpm:146,defaultBpm:140},scales:[{name:`natural_minor`,weight:40},{name:`phrygian_dominant`,weight:25},{name:`minor_pentatonic`,weight:20},{name:`aeolian`,weight:15}],chordRules:{cadenceWeight:.8,movementWeight:.35,suspendedChance:.12,brightBorrowChance:.04},rhythmRules:{kickDensity:.7,hatDensity:.55,percussionDensity:.12,fillChance:.35,halfTimeChance:1},bassRules:{density:.25,slideChance:.3,accentChance:.3,octaveJumpChance:.05,sustainChance:1},melodyRules:{density:.18,mutationChance:.2,callResponseChance:.5,maxRangeSemitones:12},arrangementRules:{minSectionBars:8,maxSectionBars:16,futureBars:8,resetChance:.05},automationRules:{filterMotion:.5,sendMotion:.3,panMotion:.2},patches:[{track:`kick`,wave:`sine`,gain:1,pan:0,attackMs:0,decayMs:120,sustain:0,releaseMs:10,filterCutoff:120,resonance:.1,delaySend:0,reverbSend:0},{track:`snare`,wave:`noise`,gain:.88,pan:0,attackMs:0,decayMs:110,sustain:0,releaseMs:40,filterCutoff:5e3,resonance:.2,delaySend:.04,reverbSend:.3},{track:`closed_hat`,wave:`noise`,gain:.4,pan:.1,attackMs:0,decayMs:35,sustain:0,releaseMs:10,filterCutoff:9e3,resonance:.2,delaySend:.05,reverbSend:.06,width:.5},{track:`open_hat`,wave:`noise`,gain:.3,pan:.2,attackMs:0,decayMs:240,sustain:0,releaseMs:50,filterCutoff:8500,resonance:.2,delaySend:.1,reverbSend:.12,width:.5},{track:`percussion`,wave:`triangle`,gain:.3,pan:-.2,attackMs:1,decayMs:80,sustain:0,releaseMs:20,filterCutoff:2800,resonance:.3,delaySend:.18,reverbSend:.2,width:.7},{track:`bass`,wave:`sine`,gain:.95,pan:0,attackMs:8,decayMs:260,sustain:.85,releaseMs:140,filterCutoff:420,resonance:.2,delaySend:0,reverbSend:0,duck:.55},{track:`chords`,wave:`saw`,gain:.3,pan:-.1,attackMs:4,decayMs:160,sustain:.2,releaseMs:140,filterCutoff:700,resonance:.5,delaySend:.3,reverbSend:.25,detuneCents:12,filterEnvOctaves:2.2,filterEnvDecayMs:160,width:.7,duck:.3},{track:`pad`,wave:`saw`,gain:.4,pan:0,attackMs:30,decayMs:600,sustain:.8,releaseMs:260,filterCutoff:320,resonance:.55,delaySend:.06,reverbSend:.2,detuneCents:16,lfoRate:2,lfoDepth:1.9,width:.6,duck:.5},{track:`lead`,wave:`square`,gain:.31,pan:.1,attackMs:6,decayMs:150,sustain:.2,releaseMs:160,filterCutoff:1700,resonance:.35,delaySend:.34,reverbSend:.3,detuneCents:8,filterEnvOctaves:1.2,filterEnvDecayMs:200,duck:.2},{track:`arp`,wave:`pulse`,gain:.2,pan:.18,attackMs:2,decayMs:70,sustain:.1,releaseMs:60,filterCutoff:2600,resonance:.3,delaySend:.3,reverbSend:.22,width:.8,duck:.2},{track:`fx`,wave:`noise`,gain:.94,pan:0,attackMs:0,decayMs:600,sustain:0,releaseMs:120,filterCutoff:5500,resonance:.2,delaySend:.25,reverbSend:.4}],mixer:{masterGain:.62,limiterDrive:1,delayFeedback:.32,reverbMix:.22,tracks:[{track:`kick`,gain:1,pan:0},{track:`snare`,gain:2,pan:0},{track:`closed_hat`,gain:1.3,pan:.12},{track:`open_hat`,gain:.93,pan:.2},{track:`percussion`,gain:1.5,pan:-.24},{track:`bass`,gain:.66,pan:0},{track:`chords`,gain:.74,pan:-.12},{track:`pad`,gain:.8,pan:0},{track:`lead`,gain:1.1,pan:.08},{track:`arp`,gain:1.2,pan:.2},{track:`fx`,gain:2,pan:0}]},limits:{minSampleRate:22050,maxSampleRate:96e3,minBpm:90,maxBpm:160,maxTracks:11,maxVoices:48,maxBarsQueued:16,maxEventsPerBlock:128}}},h={min:0,max:1},g={min:0,max:1,optional:!0},_={min:0,max:2},v={min:-1,max:1},y={min:40,max:240},b={min:1,max:64,integer:!0},x={min:0,max:1e4},S={tempo:{minBpm:y,maxBpm:y,defaultBpm:y},chordRules:{cadenceWeight:{min:0,max:4},movementWeight:{min:0,max:4},suspendedChance:h,brightBorrowChance:h},rhythmRules:{kickDensity:h,hatDensity:h,percussionDensity:h,fillChance:h,halfTimeChance:g},bassRules:{density:h,slideChance:h,accentChance:h,octaveJumpChance:h,sustainChance:g},melodyRules:{density:h,mutationChance:h,callResponseChance:h,maxRangeSemitones:{min:1,max:48,integer:!0}},arrangementRules:{minSectionBars:b,maxSectionBars:b,futureBars:b,resetChance:h},automationRules:{filterMotion:h,sendMotion:h,panMotion:h},limits:{minSampleRate:{min:22050,max:96e3,integer:!0},maxSampleRate:{min:22050,max:96e3,integer:!0},minBpm:{min:1,max:1e3},maxBpm:{min:1,max:1e3},maxTracks:{min:n.length,max:n.length,integer:!0},maxVoices:{min:1,max:64,integer:!0},maxBarsQueued:{min:1,max:64,integer:!0},maxEventsPerBlock:{min:1,max:512,integer:!0}}},C={masterGain:_,limiterDrive:_,delayFeedback:h,reverbMix:h},w={gain:_,pan:v,attackMs:x,decayMs:x,sustain:h,releaseMs:x,filterCutoff:{min:20,max:2e4},resonance:h,delaySend:h,reverbSend:h,detuneCents:{min:0,max:50,optional:!0},filterEnvOctaves:{min:0,max:6,optional:!0},filterEnvDecayMs:{min:1,max:1e4,optional:!0},lfoRate:{min:0,max:8,optional:!0},lfoDepth:{min:0,max:4,optional:!0},width:g,duck:g},T={gain:_,pan:v},E=1e6,D=n,O={amigaHouse95ish(){return k(m.amigaHouse95ish)},downtempoBreakbeat(){return k(m.downtempoBreakbeat)},dubDeepHouse(){return k(m.dubDeepHouse)},dubstep(){return k(m.dubstep)}};function k(e){return structuredClone(e)}function A(e){if(!P(e))throw N(`Profile must be an object.`);for(let t of[`id`,`displayName`,`version`])if(typeof e[t]!=`string`||e[t].trim()===``)throw N(`Profile ${t} is required and must be a non-empty string.`);for(let[t,n]of Object.entries(S))F(e[t],n,t);let t=e,{tempo:n,limits:i,arrangementRules:a}=t;if(n.minBpm>n.defaultBpm||n.defaultBpm>n.maxBpm)throw N(`tempo must be ordered: minBpm <= defaultBpm <= maxBpm.`);if(n.minBpm<i.minBpm||n.maxBpm>i.maxBpm)throw N(`tempo range must stay inside limits.minBpm and limits.maxBpm.`);if(i.minSampleRate>i.maxSampleRate)throw N(`limits.minSampleRate must not exceed limits.maxSampleRate.`);if(a.minSectionBars>a.maxSectionBars)throw N(`arrangementRules.minSectionBars must not exceed maxSectionBars.`);if(a.futureBars>i.maxBarsQueued)throw N(`arrangementRules.futureBars must not exceed limits.maxBarsQueued.`);I(t.scales),L(t.patches,w,`patches`);for(let e of t.patches)if(!r.includes(e.wave))throw N(`patches wave must be one of ${r.join(`, `)}.`);return F(t.mixer,C,`mixer`),L(t.mixer.tracks,T,`mixer.tracks`),k(t)}function j(e){if(e===void 0)return 1n;if(typeof e==`bigint`)return BigInt.asUintN(64,e);if(typeof e==`number`){if(!Number.isSafeInteger(e)||e<0)throw N(`Seed numbers must be non-negative safe integers. Use bigint or string for larger seeds.`);return BigInt(e)}if(typeof e!=`string`||!/^\d+$/.test(e))throw N(`Seed strings must contain only decimal digits.`);return BigInt.asUintN(64,BigInt(e))}function M(e){let t=BigInt.asUintN(64,e);return{high:Number(t>>32n&4294967295n),low:Number(t&4294967295n)}}function N(t){return new e(`profile_invalid`,t)}function P(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function F(e,t,n){if(!P(e))throw N(`Profile ${n} is required and must be an object.`);for(let[r,i]of Object.entries(t)){let t=e[r];if(!(t===void 0&&i.optional)&&!(typeof t==`number`&&Number.isFinite(t)&&t>=i.min&&t<=i.max&&(!i.integer||Number.isInteger(t))))throw N(`${n}.${r} must be ${i.integer?`a whole number`:`a number`} ${i.min===i.max?`exactly ${i.min}`:`from ${i.min} to ${i.max}`}.`)}}function I(e){if(!Array.isArray(e)||e.length===0||e.length>8)throw N(`Profile scales must list between 1 and 8 scales.`);for(let t of e){if(!P(t)||!i.includes(t.name))throw N(`Scale name must be one of ${i.join(`, `)}.`);let e=t.weight;if(!Number.isInteger(e)||e<1||e>1e6)throw N(`Scale weight must be a whole number from 1 to ${E}.`)}}function L(e,t,n){if(!Array.isArray(e)||e.length!==D.length)throw N(`Profile ${n} must include one entry for each built-in track.`);let r=new Set;for(let i of e){if(F(i,t,n),!D.includes(i.track))throw N(`${n} track must be one of ${D.join(`, `)}.`);r.add(i.track)}if(r.size!==D.length)throw N(`Profile ${n} must not repeat a track.`)}async function R(t){let n=t.toString(),r={},i=await fetch(n);if(!i.ok)throw new e(`wasm_load_failed`,`Could not load AlgoWASM from ${n}. The server returned ${i.status}.`);if(WebAssembly.instantiateStreaming)try{let e=await WebAssembly.instantiateStreaming(i,r);return{instance:e.instance,module:e.module}}catch(t){if(i.headers.get(`content-type`)===`application/wasm`)throw new e(`wasm_load_failed`,`WASM streaming compilation failed even though the server served application/wasm.`,{cause:t})}let a=await fetch(n);if(!a.ok)throw new e(`wasm_load_failed`,`Could not load AlgoWASM fallback bytes from ${n}. The server returned ${a.status}.`);let o=await a.arrayBuffer(),s=await WebAssembly.instantiate(o,r);return{instance:s.instance,module:s.module}}function z(e){let t=a.indexOf(e);if(t<0)throw TypeError(`Expected mood to be one of ${a.join(`, `)}.`);return t}function B(e){let t=n.indexOf(e);if(t<0)throw TypeError(`Expected trackId to be one of ${n.join(`, `)}.`);return t}var V=class extends p{create(e,t,n){let{high:r,low:i}=M(t);this.createFromWire(e,r,i,l(A(n)))}setSeed(e){let{high:t,low:n}=M(e);this.setSeedParts(t,n)}setProfile(e){this.setProfileWire(l(A(e)))}setMood(e){this.setMoodIndex(z(e))}setMuted(e,t){this.setMutedIndex(B(e),t)}setSolo(e,t){this.setSoloIndex(B(e),t)}snapshot(){return d(this.copySnapshot())}events(){let e=this.copyEvents();return e?f(e):[]}},H=600,U=8192,W=class n{emitter=new t;audioContext;ownsAudioContext;node;wasmUrl;pending=new Map;commandId=1;destroyed=!1;destroyPromise;offlineModule;profile;seed;lastSnapshot;lastEvents=[];volume=.8;energy=.5;intensity=.5;mood=`dark`;tempoRange;muted=new Set;solo=new Set;constructor(e,t,n,r,i,a){this.audioContext=e,this.ownsAudioContext=t,this.node=n,this.wasmUrl=r,this.profile=i,this.seed=a,this.lastSnapshot=q(i,a,e.sampleRate),this.node.port.onmessage=e=>this.handleMessage(e.data)}static async create(t){if(K(),!t?.wasmUrl||!t.workletUrl)throw TypeError(`Expected options.wasmUrl and options.workletUrl to be a string or URL.`);let r=A(t.profile??O.amigaHouse95ish()),i=j(t.seed),a=t.wasmUrl.toString(),o=t.workletUrl.toString(),s=!t.audioContext,c=t.audioContext??new AudioContext,l;try{if(!c.audioWorklet)throw new e(`browser_feature_missing`,`AudioWorklet is not available in this browser. AlgoWASM targets modern browsers from 2025 onwards.`);try{await c.audioWorklet.addModule(o)}catch(t){throw new e(`worklet_load_failed`,`Could not load the AlgoWASM worklet from ${o}.`,{cause:t})}l=new AudioWorkletNode(c,`algo-wasm-processor`,{numberOfInputs:0,numberOfOutputs:1,outputChannelCount:[2]}),l.connect(c.destination);let u=new n(c,s,l,a,r,i);return u.volume=J(t.volume??.8),await u.configure(),u}catch(e){throw l&&(l.port.postMessage({type:`destroy`}),l.disconnect(),l.port.close()),s&&c.state!==`closed`&&await c.close().catch(()=>{}),e}}async start(){this.assertAlive();try{await Y(this.audioContext)===`pending`&&this.emitter.emit(`warning`,`AudioContext did not report a running state yet. Playback will continue when the browser unlocks audio.`)}catch(t){throw new e(`audio_context_locked`,`AudioContext could not resume. Call start() from a user gesture such as a click or tap.`,{cause:t})}await this.sendCommand(`start`),this.emitter.emit(`started`,this.lastSnapshot)}async pause(){this.assertAlive(),await this.sendCommand(`pause`),this.emitter.emit(`paused`,this.lastSnapshot)}async resume(){this.assertAlive(),await Y(this.audioContext)===`pending`&&this.emitter.emit(`warning`,`AudioContext did not report a running state yet. Playback will continue when the browser unlocks audio.`),await this.sendCommand(`resume`),this.emitter.emit(`resumed`,this.lastSnapshot)}async stop(){this.assertAlive(),await this.sendCommand(`stop`),this.emitter.emit(`stopped`,this.lastSnapshot)}async destroy(){return this.destroyPromise||=this.performDestroy(),this.destroyPromise}async performDestroy(){try{await this.sendBestEffort(`destroy`)}finally{this.destroyed=!0;for(let t of this.pending.values())clearTimeout(t.timeout),t.reject(new e(`destroyed`,`AlgoWASM player was destroyed.`));if(this.pending.clear(),this.node.disconnect(),this.node.port.close(),this.emitter.clear(),this.ownsAudioContext&&this.audioContext.state!==`closed`)try{await this.audioContext.close()}catch{}}}setSeed(e){this.seed=j(e);let{high:t,low:n}=M(this.seed);this.postLiveCommand(`set-seed`,{seedHigh:t,seedLow:n})}setProfile(e){let t=A(e);this.postLiveCommand(`set-profile`,{profile:l(t)},()=>{this.profile=t,this.tempoRange=void 0})}setVolume(e){this.volume=J(e),this.postLiveCommand(`set-volume`,{value:this.volume})}setMuted(e,t){let n=B(e);G(this.muted,e,t),this.postLiveCommand(`set-muted`,{track:n,flag:!!t})}setSolo(e,t){let n=B(e);G(this.solo,e,t),this.postLiveCommand(`set-solo`,{track:n,flag:!!t})}setEnergy(e){this.energy=J(e),this.postLiveCommand(`set-energy`,{value:this.energy})}setIntensity(e){this.intensity=J(e),this.postLiveCommand(`set-intensity`,{value:this.intensity})}setMood(e){let t=z(e);this.mood=e,this.postLiveCommand(`set-mood`,{mood:t})}setTempoRange(e,t){if(!Number.isFinite(e)||!Number.isFinite(t))throw TypeError(`Expected minBpm and maxBpm to be finite numbers.`);if(e>t)throw RangeError(`Expected minBpm to be less than or equal to maxBpm.`);this.postLiveCommand(`set-tempo-range`,{minBpm:e,maxBpm:t},()=>{this.tempoRange={minBpm:e,maxBpm:t}})}getSnapshot(){return structuredClone(this.lastSnapshot)}getCurrentEvents(){return structuredClone(this.lastEvents)}async exportMidi(e={}){this.assertAlive();let t=new Uint8Array(await this.sendCommand(`export-midi`));if(e.downloadName&&typeof document<`u`){let n=URL.createObjectURL(new Blob([t],{type:`audio/midi`})),r=document.createElement(`a`);r.href=n,r.download=e.downloadName,r.click(),URL.revokeObjectURL(n)}return t}async renderOffline(e){this.assertAlive();let t=e?.seconds,n=e?.sampleRate??this.audioContext.sampleRate;if(typeof t!=`number`||!(t>=0&&t<=H))throw RangeError(`Expected seconds to be a number from 0 to ${H}.`);if(!Number.isInteger(n)||n<22050||n>96e3)throw RangeError(`Expected sampleRate to be a whole number from 22050 to 96000.`);let r=A(e.profile??this.profile),i=j(e.seed??this.seed),a=new Float32Array(Math.floor(t*n)*2),o=new V(await WebAssembly.instantiate(await this.compileOffline(),{}));try{o.create(n,i,r),o.setVolume(this.volume),o.setEnergy(this.energy),o.setIntensity(this.intensity),o.setMood(this.mood),this.tempoRange&&!e.profile&&o.setTempoRange(this.tempoRange.minBpm,this.tempoRange.maxBpm);for(let e of this.muted)o.setMuted(e,!0);for(let e of this.solo)o.setSolo(e,!0);o.start();let t=performance.now();for(let e=0;e<a.length;e+=U*2)o.renderTo(a.subarray(e,e+U*2)),performance.now()-t>12&&(await new Promise(e=>setTimeout(e)),t=performance.now())}finally{o.destroy()}return a}on(e,t){return this.emitter.on(e,t)}off(e,t){this.emitter.off(e,t)}compileOffline(){return this.offlineModule??=R(this.wasmUrl).then(e=>e.module,e=>{throw this.offlineModule=void 0,e}),this.offlineModule}async configure(){let t=await X(this.wasmUrl),n=new Promise((t,n)=>{let r=()=>{},i=()=>{},a=setTimeout(()=>{r(),i(),n(new e(`engine_failed`,`AlgoWASM worklet did not become ready within five seconds.`))},5e3);r=this.on(`ready`,()=>{clearTimeout(a),r(),i(),t()}),i=this.on(`error`,e=>{clearTimeout(a),r(),i(),n(e)})}),{high:r,low:i}=M(this.seed);this.node.port.postMessage({type:`configure`,wasmBytes:t,profile:l(this.profile),seedHigh:r,seedLow:i,value:this.volume,sampleRate:this.audioContext.sampleRate},[t]),await n}postLiveCommand(t,n,r){if(this.destroyed)throw new e(`destroyed`,`AlgoWASM player has been destroyed.`);this.sendCommand(t,n).then(r).catch(e=>{this.emitter.emit(`error`,e instanceof Error?e:Error(String(e)))})}async sendBestEffort(e,t={}){try{await this.sendCommand(e,t,1e3)}catch{}}sendCommand(t,n={},r=5e3){this.assertAlive();let i=this.commandId++;return new Promise((a,o)=>{let s=setTimeout(()=>{this.pending.delete(i),o(new e(`engine_failed`,`Worklet command ${t} timed out.`))},r);this.pending.set(i,{resolve:a,reject:o,timeout:s}),this.node.port.postMessage({type:t,id:i,...n})})}handleMessage(t){if(t.type===`ready`){this.lastSnapshot=d(t.snapshot),this.emitter.emit(`ready`,this.lastSnapshot);return}if(t.type===`events`){this.lastEvents=f(t.events),this.lastSnapshot=d(t.snapshot);for(let e of this.lastEvents)(e.type===`section-change`||e.type===`bar`||e.type===`beat`)&&this.emitter.emit(e.type,e);return}if(t.type===`error`){this.emitter.emit(`error`,new e(`engine_failed`,t.error));return}let n=this.pending.get(t.id);n&&(clearTimeout(n.timeout),this.pending.delete(t.id),t.ok?(t.snapshot&&(this.lastSnapshot=d(t.snapshot)),n.resolve(t.result)):n.reject(new e(`engine_failed`,t.error??`Worklet command failed.`)))}assertAlive(){if(this.destroyed)throw new e(`destroyed`,`AlgoWASM player has been destroyed.`)}};function G(e,t,n){n?e.add(t):e.delete(t)}function K(){if(typeof AudioContext>`u`)throw new e(`browser_feature_missing`,`AudioContext is not available in this browser. AlgoWASM targets modern browsers from 2025 onwards.`);if(typeof WebAssembly>`u`)throw new e(`browser_feature_missing`,`WebAssembly is not available in this browser. AlgoWASM cannot run here.`)}function q(e,t,n){return{seed:t.toString(),sampleRate:n,bpm:e.tempo.defaultBpm,currentBar:0,currentBeat:1,currentSection:`intro`,energy:.5,intensity:.5,isPlaying:!1,tracks:e.mixer.tracks.map(e=>({id:e.track,muted:!1,solo:!1,level:e.gain}))}}function J(e){return Number.isFinite(e)?Math.min(1,Math.max(0,e)):0}async function Y(e){return e.state===`running`?`running`:Promise.race([e.resume().then(()=>`running`),new Promise(e=>{setTimeout(()=>e(`pending`),1200)})])}async function X(t){let n=await fetch(t);if(!n.ok)throw new e(`wasm_load_failed`,`Could not load AlgoWASM from ${t}. The server returned ${n.status}.`);return n.arrayBuffer()}export{V as AlgoWasmEngineBinding,e as AlgoWasmError,t as AlgoWasmEventEmitter,W as AlgoWasmPlayer,O as builtInProfiles,k as cloneProfile,R as instantiateAlgoWasm,j as normaliseSeed,M as splitSeed,D as trackIds,A as validateProfile};
@@ -1,4 +1,5 @@
1
- import type { AlgoWasmPlayer } from '../AlgoWasmPlayer.js';
1
+ import type { AlgoWasmPlayer } from '../algo-wasm-player.js';
2
+ /** The parts of a Phaser scene this integration reads. */
2
3
  export interface PhaserLikeScene {
3
4
  registry?: {
4
5
  get(key: string): unknown;
@@ -7,5 +8,11 @@ export interface PhaserLikeScene {
7
8
  once(eventName: string, handler: () => void): void;
8
9
  };
9
10
  }
11
+ /**
12
+ * Drives the player's energy from a number in the scene registry, read once
13
+ * per animation frame. A missing or non-numeric value counts as 0.
14
+ *
15
+ * Returns a function that stops the updates. Call it when the scene shuts
16
+ * down, or the loop keeps running.
17
+ */
10
18
  export declare function bindAlgoWasmToPhaserDanger(scene: PhaserLikeScene, player: AlgoWasmPlayer, registryKey?: string): () => void;
11
- //# sourceMappingURL=phaser.d.ts.map
@@ -1,16 +1 @@
1
- export function bindAlgoWasmToPhaserDanger(scene, player, registryKey = 'danger') {
2
- let disposed = false;
3
- const update = () => {
4
- if (disposed) {
5
- return;
6
- }
7
- const value = scene.registry?.get(registryKey);
8
- player.setEnergy(typeof value === 'number' ? value : 0);
9
- requestAnimationFrame(update);
10
- };
11
- update();
12
- return () => {
13
- disposed = true;
14
- };
15
- }
16
- //# sourceMappingURL=phaser.js.map
1
+ function e(e,t,n=`danger`){let r=!1,i=()=>{if(r)return;let a=e.registry?.get(n);t.setEnergy(typeof a==`number`?a:0),requestAnimationFrame(i)};return i(),()=>{r=!0}}export{e as bindAlgoWasmToPhaserDanger};
@@ -1,8 +1,14 @@
1
- import type { AlgoWasmPlayer } from '../AlgoWasmPlayer.js';
1
+ import type { AlgoWasmPlayer } from '../algo-wasm-player.js';
2
2
  import type { AlgoWasmSnapshot } from '../types.js';
3
+ /** A store in the shape React's `useSyncExternalStore` expects. */
3
4
  export interface AlgoWasmExternalStore {
4
5
  getSnapshot(): AlgoWasmSnapshot;
5
6
  subscribe(listener: () => void): () => void;
6
7
  }
8
+ /**
9
+ * Wraps a player as an external store that notifies on each bar, start, and
10
+ * stop. Pass `subscribe` and `getSnapshot` to `useSyncExternalStore`.
11
+ *
12
+ * The player listeners are removed when the last subscriber unsubscribes.
13
+ */
7
14
  export declare function createAlgoWasmExternalStore(player: AlgoWasmPlayer): AlgoWasmExternalStore;
8
- //# sourceMappingURL=react.d.ts.map
@@ -1,28 +1 @@
1
- export function createAlgoWasmExternalStore(player) {
2
- const listeners = new Set();
3
- const notify = () => {
4
- for (const listener of listeners) {
5
- listener();
6
- }
7
- };
8
- const offBar = player.on('bar', notify);
9
- const offStarted = player.on('started', notify);
10
- const offStopped = player.on('stopped', notify);
11
- return {
12
- getSnapshot() {
13
- return player.getSnapshot();
14
- },
15
- subscribe(listener) {
16
- listeners.add(listener);
17
- return () => {
18
- listeners.delete(listener);
19
- if (listeners.size === 0) {
20
- offBar();
21
- offStarted();
22
- offStopped();
23
- }
24
- };
25
- }
26
- };
27
- }
28
- //# sourceMappingURL=react.js.map
1
+ function e(e){let t=new Set,n=()=>{for(let e of t)e()},r=e.on(`bar`,n),i=e.on(`started`,n),a=e.on(`stopped`,n);return{getSnapshot(){return e.getSnapshot()},subscribe(e){return t.add(e),()=>{t.delete(e),t.size===0&&(r(),i(),a())}}}}export{e as createAlgoWasmExternalStore};
@@ -1,7 +1,11 @@
1
- import type { AlgoWasmPlayer } from '../AlgoWasmPlayer.js';
1
+ import type { AlgoWasmPlayer } from '../algo-wasm-player.js';
2
2
  import type { AlgoWasmSnapshot } from '../types.js';
3
+ /** A store that follows Svelte's readable store contract. */
3
4
  export interface AlgoWasmReadableStore {
4
5
  subscribe(run: (snapshot: AlgoWasmSnapshot) => void): () => void;
5
6
  }
7
+ /**
8
+ * Wraps a player as a Svelte readable store of snapshots, updated on each
9
+ * bar, start, and stop. Unsubscribing removes that subscriber's listeners.
10
+ */
6
11
  export declare function readableAlgoWasmPlayer(player: AlgoWasmPlayer): AlgoWasmReadableStore;
7
- //# sourceMappingURL=svelte.d.ts.map
@@ -1,17 +1 @@
1
- export function readableAlgoWasmPlayer(player) {
2
- return {
3
- subscribe(run) {
4
- run(player.getSnapshot());
5
- const update = () => run(player.getSnapshot());
6
- const offBar = player.on('bar', update);
7
- const offStarted = player.on('started', update);
8
- const offStopped = player.on('stopped', update);
9
- return () => {
10
- offBar();
11
- offStarted();
12
- offStopped();
13
- };
14
- }
15
- };
16
- }
17
- //# sourceMappingURL=svelte.js.map
1
+ function e(e){return{subscribe(t){t(e.getSnapshot());let n=()=>t(e.getSnapshot()),r=e.on(`bar`,n),i=e.on(`started`,n),a=e.on(`stopped`,n);return()=>{r(),i(),a()}}}}export{e as readableAlgoWasmPlayer};
@@ -1,8 +1,13 @@
1
- import type { AlgoWasmPlayer } from '../AlgoWasmPlayer.js';
1
+ import type { AlgoWasmPlayer } from '../algo-wasm-player.js';
2
2
  import type { AlgoWasmSnapshot } from '../types.js';
3
+ /** The handle returned by `createAlgoWasmVueBridge`. */
3
4
  export interface AlgoWasmVueBridge {
4
5
  snapshot: AlgoWasmSnapshot;
5
6
  dispose(): void;
6
7
  }
8
+ /**
9
+ * Calls `assign` with a fresh snapshot now and on each bar, start, and stop,
10
+ * so a Vue ref can mirror the player. Call `dispose` when the component
11
+ * unmounts to remove the listeners.
12
+ */
7
13
  export declare function createAlgoWasmVueBridge(player: AlgoWasmPlayer, assign: (snapshot: AlgoWasmSnapshot) => void): AlgoWasmVueBridge;
8
- //# sourceMappingURL=vue.d.ts.map
@@ -1,16 +1 @@
1
- export function createAlgoWasmVueBridge(player, assign) {
2
- const update = () => assign(player.getSnapshot());
3
- const offBar = player.on('bar', update);
4
- const offStarted = player.on('started', update);
5
- const offStopped = player.on('stopped', update);
6
- update();
7
- return {
8
- snapshot: player.getSnapshot(),
9
- dispose() {
10
- offBar();
11
- offStarted();
12
- offStopped();
13
- }
14
- };
15
- }
16
- //# sourceMappingURL=vue.js.map
1
+ function e(e,t){let n=()=>t(e.getSnapshot()),r=e.on(`bar`,n),i=e.on(`started`,n),a=e.on(`stopped`,n);return n(),{snapshot:e.getSnapshot(),dispose(){r(),i(),a()}}}export{e as createAlgoWasmVueBridge};