@datamoc/mw_games 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,45 @@ All notable changes to `mwg` are documented here. Format follows
5
5
  [Semantic Versioning](https://semver.org/) as of this first release - a 0.y.z version means
6
6
  the public API may still change between minor versions.
7
7
 
8
+ ## [0.18.0] - 2026-09-25
9
+
10
+ ### Added
11
+
12
+ - `audio.parseMidi` keeps program-change, control-change and pitch-bend events and reports
13
+ the controller-111 loop marker as `loopStartTick`; `scheduleMidi` notes carry their channel
14
+ voice (`MidiVoice`: program, bank, gain, pan) and `midiLoopStart` resolves the marker to seconds.
15
+ - `audio.renderMidiToBuffer` renders a parsed MIDI file to stereo PCM frames, offline and
16
+ deterministic, voiced by an optional game-supplied `SoundFont` or the built-in waveform
17
+ synth, with the loop region attached.
18
+ - `audio.parseSoundFont` reads SoundFont 2 bytes into layered preset voices.
19
+ - `audio.Channel` (a seekable buffered WebAudio voice with volume, pitch, pan and a loop
20
+ region) and `audio.AudioBus` (independent BGM/BGS loops, ME jingles that suspend and resume
21
+ the BGM, overlapping SE one-shots, save and replay with playheads, plain-data snapshots).
22
+ ## [0.17.1] - 2026-09-25
23
+
24
+ ### Fixed
25
+
26
+ - A page built on a Windows checkout refused its own inline script. The HTML parser turns
27
+ newlines into LF before it hashes an inline script, while `contentSecurityPolicy` hashed the
28
+ CRLF bytes a `core.autocrlf=true` checkout hands over, so Chrome computed a different hash
29
+ from the one the policy allowed. The page logged a Content-Security-Policy violation that
30
+ `mwg-smoke`, the visual smoke and `mwg-bench` read as a page error, the source-page fallback
31
+ script never ran, and the same build on Linux stayed quiet - which is why no browser job had
32
+ caught it. Inline scripts are now hashed after CRLF and lone CR become LF; the standalone
33
+ page's unpacker scripts stay hashed as written, since no parser touches those. A CRLF
34
+ checkout and an LF checkout now produce the same policy, pinned by a test.
35
+ - The committed-artifact checks no longer read the checkout's line endings. `api:check`,
36
+ `stats:check`, `sbom:check` and the committed-SBOM test compared generated text against the
37
+ committed file byte for byte, so a Windows clone failed them on a tree `git diff` called
38
+ clean; `bundle.distRaw` summed the bytes `tsc` copies out of a source file's comments, which
39
+ recorded who generated the file rather than what ships.
40
+
41
+ ### Changed
42
+
43
+ - The `check` job runs on `windows-latest` alongside `ubuntu-latest`, so the two failures above
44
+ fail the push instead of reaching a Windows contributor. The gates themselves are unchanged,
45
+ and `fail-fast` is off so one OS failing still shows the other.
46
+
8
47
  ## [0.17.0] - 2026-09-25
9
48
 
10
49
  ### Security
package/README.md CHANGED
@@ -15,7 +15,7 @@ playable in the browser with no download, plus the generated API reference.
15
15
  planning or porting a demanding game with mwg, with links to the [mwg reference](https://github.com/datamoc/mwg/blob/main/REFERENCE.md)
16
16
  and the [mwg Pixel Dungeon study](https://datamoc.github.io/mwg-pixel-dungeon/).
17
17
 
18
- > **Status: pre-alpha (v0.17.0).** Every module in the shared floor below, plus optional
18
+ > **Status: pre-alpha (v0.17.1).** Every module in the shared floor below, plus optional
19
19
  > 3D, mobile (Capacitor) and desktop (WebView2) packaging, is built and tested - see
20
20
  > [ROADMAP.md](https://github.com/datamoc/mwg/blob/main/ROADMAP.md) for open work, and
21
21
  > [CLOSED.md](https://github.com/datamoc/mwg/blob/main/CLOSED.md) for the full, numbered shipped history.
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Buffered WebAudio voices for music and jingles: a `Channel` plays one looping track
3
+ * with volume, pitch, pan, a loop region and a seekable playhead, and an `AudioBus`
4
+ * wires channels into the four lifetimes a role-playing game needs from its audio.
5
+ * BGM and BGS are two independent looping streams, ME is an interrupting jingle that
6
+ * pauses the BGM and resumes it where it left off, SE is overlapping one-shots, each
7
+ * with its own volume, pitch and pan per play.
8
+ *
9
+ * The browser owns the clock and the nodes, so both classes take the pieces they
10
+ * cannot build themselves as injectables: the `AudioContext` in the constructor and,
11
+ * for the bus, a `load` function turning a kind and a name into a decoded buffer.
12
+ * Tests hand in fakes for both, the same way `Sound` and `Music` take `create()`.
13
+ */
14
+ /** seconds within a buffer: the region a looping track repeats */
15
+ export interface LoopRegion {
16
+ start: number;
17
+ end: number;
18
+ }
19
+ export interface ChannelPlayOptions {
20
+ /** level, 0 to 1 */
21
+ volume?: number;
22
+ /** playback ratio, 1 unchanged, above 1 higher and faster */
23
+ pitch?: number;
24
+ /** stereo position, -1 (left) to 1 (right) */
25
+ pan?: number;
26
+ /** start offset in seconds */
27
+ pos?: number;
28
+ /** region to repeat, null repeats the whole buffer */
29
+ loop?: LoopRegion | null;
30
+ /** false plays once through for jingles; true repeats */
31
+ repeat?: boolean;
32
+ }
33
+ /** plain-data playback state: parameters plus playhead, without the buffer */
34
+ export interface ChannelState {
35
+ volume: number;
36
+ pitch: number;
37
+ pan: number;
38
+ pos: number;
39
+ }
40
+ /**
41
+ * One buffered voice: a decoded track with a loop region and a playhead the game can
42
+ * read and move. Replaying the same buffer restarts it; changing parameters mid-play
43
+ * never does.
44
+ *
45
+ * @example
46
+ * ```ts
47
+ * import { Channel } from '@datamoc/mw_games/audio';
48
+ *
49
+ * declare const context: AudioContext;
50
+ * declare const buffer: AudioBuffer;
51
+ *
52
+ * const channel = new Channel(context);
53
+ * channel.play(buffer, { volume: 0.8, pitch: 1, pan: 0, loop: { start: 1, end: 4 } });
54
+ * console.log(channel.position());
55
+ * channel.stop();
56
+ * ```
57
+ */
58
+ export declare class Channel {
59
+ private readonly context;
60
+ private readonly destination;
61
+ private source;
62
+ private gainNode;
63
+ private pannerNode;
64
+ private buffer;
65
+ private params;
66
+ private loopRegion;
67
+ private repeat;
68
+ private startedAt;
69
+ private offset;
70
+ constructor(context: AudioContext, destination?: AudioNode);
71
+ get isPlaying(): boolean;
72
+ get duration(): number;
73
+ /** starts the buffer with these parameters, stopping whatever played before */
74
+ play(buffer: AudioBuffer, options?: ChannelPlayOptions): void;
75
+ /** retunes the playing voice in place: no restart, no seek */
76
+ update(params: {
77
+ volume?: number;
78
+ pitch?: number;
79
+ pan?: number;
80
+ }): void;
81
+ /** stops the voice, fading out over `fadeSeconds` first when positive */
82
+ stop(fadeSeconds?: number): void;
83
+ /** seconds into the buffer, wrapping inside the loop region while it repeats */
84
+ position(): number;
85
+ /** moves the playhead, restarting the voice when it plays */
86
+ seek(pos: number): void;
87
+ /** plain-data state for saves and transfers: parameters plus playhead */
88
+ state(): ChannelState;
89
+ /** applies plain-data state, restarting the voice at its playhead when it plays */
90
+ restore(state: ChannelState): void;
91
+ private clearNodes;
92
+ }
93
+ export type BusKind = 'bgm' | 'bgs' | 'me' | 'se';
94
+ /**
95
+ * A named track request in role-playing units: volume 0 to 100, pitch 50 to 150
96
+ * percent, pan -100 to 100, an optional resume position in seconds.
97
+ */
98
+ export interface BusTrack {
99
+ name: string;
100
+ volume?: number;
101
+ pitch?: number;
102
+ pan?: number;
103
+ pos?: number;
104
+ }
105
+ /** a resolved track request: every parameter filled in, ready to store or restore */
106
+ export interface SavedBusTrack {
107
+ name: string;
108
+ volume: number;
109
+ pitch: number;
110
+ pan: number;
111
+ pos: number;
112
+ }
113
+ /** a decoded track: its buffer plus the region it repeats, if any */
114
+ export interface LoadedTrack {
115
+ buffer: AudioBuffer;
116
+ loop?: LoopRegion | null;
117
+ }
118
+ /** turns a kind and a name into a decoded buffer, or null when it cannot */
119
+ export type BusLoader = (kind: BusKind, name: string) => Promise<LoadedTrack | null>;
120
+ /** plain-data bus state for saves and transfers: tracks plus memorized ones */
121
+ export interface BusSnapshot {
122
+ bgm: SavedBusTrack | null;
123
+ bgs: SavedBusTrack | null;
124
+ savedBgm: SavedBusTrack | null;
125
+ savedBgs: SavedBusTrack | null;
126
+ }
127
+ export interface AudioBusOptions {
128
+ destination?: AudioNode;
129
+ load?: BusLoader;
130
+ }
131
+ /**
132
+ * The four audio lifetimes of a role-playing game over two `Channel` voices: BGM and
133
+ * BGS loop independently, an ME jingle suspends the BGM and resumes it where it left
134
+ * off, SE plays overlapping one-shots. Replaying the current track only retunes it;
135
+ * save and replay memorize a track with its playhead for menus and transfers.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * import { AudioBus } from '@datamoc/mw_games/audio';
140
+ *
141
+ * declare const context: AudioContext;
142
+ *
143
+ * const bus = new AudioBus(context, { load: (_kind, _name) => Promise.resolve(null) });
144
+ * void bus.playBgm({ name: 'town', volume: 90, pitch: 100, pan: 0 });
145
+ * console.log(bus.snapshot());
146
+ * ```
147
+ */
148
+ export declare class AudioBus {
149
+ private readonly context;
150
+ private readonly destination;
151
+ private readonly load;
152
+ private readonly bgmChannel;
153
+ private readonly bgsChannel;
154
+ private readonly tracks;
155
+ private readonly saved;
156
+ private readonly requests;
157
+ private me;
158
+ private meToken;
159
+ private suspendedBgm;
160
+ private readonly seNodes;
161
+ constructor(context: AudioContext, options?: AudioBusOptions);
162
+ /** loops this BGM, or retunes it when it already loops */
163
+ playBgm(track: BusTrack): Promise<void>;
164
+ /** loops this BGS, or retunes it when it already loops */
165
+ playBgs(track: BusTrack): Promise<void>;
166
+ stopBgm(): void;
167
+ stopBgs(): void;
168
+ fadeOutBgm(seconds?: number): void;
169
+ fadeOutBgs(seconds?: number): void;
170
+ /** the intended track with its live playhead, or the one an ME suspended */
171
+ currentTrack(kind: 'bgm' | 'bgs'): SavedBusTrack | null;
172
+ /** memorizes the playing BGM with its playhead; replay restores it */
173
+ saveBgm(): SavedBusTrack | null;
174
+ /** memorizes the playing BGS with its playhead; replay restores it */
175
+ saveBgs(): SavedBusTrack | null;
176
+ replayBgm(): Promise<void>;
177
+ replayBgs(): Promise<void>;
178
+ /**
179
+ * Plays a jingle once, suspending the BGM and resuming it where it left off when
180
+ * the jingle ends. A BGM started mid-jingle takes over instead of being resumed over.
181
+ */
182
+ playMe(track: BusTrack): Promise<void>;
183
+ /** stops the jingle, resuming the suspended BGM unless told not to */
184
+ stopMe(resume?: boolean): void;
185
+ /** plays one overlapping sound effect */
186
+ playSe(track: BusTrack): Promise<void>;
187
+ stopSe(): void;
188
+ stopAll(): void;
189
+ /** plain-data bus state for saves and transfers */
190
+ snapshot(): BusSnapshot;
191
+ /** replays plain-data bus state, reloading every named track */
192
+ restore(state: BusSnapshot | null | undefined): void;
193
+ private static normalize;
194
+ private playLoop;
195
+ private stopMeNodes;
196
+ private finishMe;
197
+ }
198
+ //# sourceMappingURL=Channels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Channels.d.ts","sourceRoot":"","sources":["../../src/audio/Channels.ts"],"names":[],"mappings":"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"}