@manybitsbyte/nesplayer-svelte 0.8.0 → 0.10.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 +18 -5
- package/dist/components/Screen.svelte +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/wasm/audio-worklet-src.d.ts +1 -0
- package/dist/wasm/audio-worklet-src.js +174 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,8 +13,24 @@ npm install @manybitsbyte/nesplayer-svelte
|
|
|
13
13
|
```svelte
|
|
14
14
|
<script>
|
|
15
15
|
import { Screen } from '@manybitsbyte/nesplayer-svelte';
|
|
16
|
+
</script>
|
|
17
|
+
|
|
18
|
+
<Screen style="width: 512px; height: 480px;" />
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
That's it. The built-in controls overlay includes a file picker — hover the right edge of the player and click the disk icon to load a `.nes` ROM.
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
The component fills its container. Set width/height on the element or a wrapping div. The canvas maintains the correct NES aspect ratio (8:7 pixel, ~1.167:1) and letter-boxes within the available space.
|
|
24
|
+
|
|
25
|
+
### Loading a ROM programmatically
|
|
26
|
+
|
|
27
|
+
If you want to serve ROMs from your own UI (a game library, a URL fetch, etc.), pass the bytes via the `rom` prop:
|
|
28
|
+
|
|
29
|
+
```svelte
|
|
30
|
+
<script>
|
|
31
|
+
import { Screen } from '@manybitsbyte/nesplayer-svelte';
|
|
32
|
+
|
|
33
|
+
let romBytes = $state(null);
|
|
18
34
|
|
|
19
35
|
async function loadRom() {
|
|
20
36
|
const res = await fetch('/roms/game.nes');
|
|
@@ -22,13 +38,10 @@ npm install @manybitsbyte/nesplayer-svelte
|
|
|
22
38
|
}
|
|
23
39
|
</script>
|
|
24
40
|
|
|
25
|
-
<button onclick={loadRom}>Load
|
|
26
|
-
|
|
41
|
+
<button onclick={loadRom}>Load game</button>
|
|
27
42
|
<Screen rom={romBytes} romName="game" style="width: 512px; height: 480px;" />
|
|
28
43
|
```
|
|
29
44
|
|
|
30
|
-
The component fills its container. Set width/height on the element or a wrapping div. The canvas maintains the correct NES aspect ratio (8:7 pixel, ~1.167:1) and letter-boxes within the available space.
|
|
31
|
-
|
|
32
45
|
## Props
|
|
33
46
|
|
|
34
47
|
| Prop | Type | Default | Bindable | Description |
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { SvelteSet } from 'svelte/reactivity';
|
|
4
4
|
import createNESPlayerModule from '../wasm/nes-player.js';
|
|
5
5
|
import nesPlayerWasmUrl from '../wasm/nes-player.wasm?url';
|
|
6
|
-
import audioWorkletSrc from '../wasm/audio-worklet.js
|
|
6
|
+
import { audioWorkletSrc } from '../wasm/audio-worklet-src.js';
|
|
7
7
|
import ControllerPanel from './ControllerPanel.svelte';
|
|
8
8
|
import { version } from '../version.js';
|
|
9
9
|
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version: "0.
|
|
1
|
+
export const version: "0.10.0";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = '0.
|
|
1
|
+
export const version = '0.10.0';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const audioWorkletSrc = "\nconst RING_SIZE = 4096;\nconst PRE_BUFFER = 1024;\nconst TARGET_FILL = 1536;\nconst SMOOTH = 0.005;\nconst RATE_GAIN = 0.000008;\nconst MAX_ADJUST = 0.005;\nconst FADE_RATE = 0.97;\nconst MP_BUF_SIZE = 16384;\n\nclass NESAudioProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this.headerView = null;\n this.ringView = null;\n this.ready = false;\n this.primed = false;\n this.lastSample = 0;\n this.smoothFill = TARGET_FILL;\n this.readBuf = new Float32Array(192);\n this.mpBuf = new Float32Array(MP_BUF_SIZE);\n this.mpHead = 0;\n this.mpTail = 0;\n this.mpMode = false;\n this.mpPrimed = false;\n\n this.port.onmessage = (e) => {\n if (e.data.type === 'init') {\n const sab = e.data.sharedBuffer;\n this.headerView = new Int32Array(sab, 0, 2);\n this.ringView = new Float32Array(sab, 8, RING_SIZE);\n this.mpMode = false;\n this.ready = true;\n } else if (e.data.type === 'init-mp') {\n this.mpMode = true;\n this.ready = true;\n } else if (e.data.type === 'push') {\n const s = e.data.samples;\n const avail = MP_BUF_SIZE - (this.mpHead - this.mpTail);\n const count = Math.min(s.length, avail);\n for (let i = 0; i < count; i++) {\n this.mpBuf[(this.mpHead + i) % MP_BUF_SIZE] = s[i];\n }\n this.mpHead += count;\n }\n };\n }\n\n process(inputs, outputs) {\n const output = outputs[0];\n if (!output || output.length === 0) { return true; }\n\n const left = output[0];\n const right = output.length > 1 ? output[1] : null;\n const frames = left.length;\n\n if (!this.ready) {\n left.fill(0);\n if (right) { right.fill(0); }\n return true;\n }\n\n if (this.mpMode) {\n return this._processMp(left, right, frames);\n }\n\n const writePos = Atomics.load(this.headerView, 0);\n const readPos = Atomics.load(this.headerView, 1);\n const available = (writePos - readPos + RING_SIZE) % RING_SIZE;\n\n if (!this.primed) {\n if (available < PRE_BUFFER) {\n left.fill(0);\n if (right) { right.fill(0); }\n return true;\n }\n this.primed = true;\n this.smoothFill = available;\n }\n\n if (available === 0) {\n let held = this.lastSample;\n for (let i = 0; i < frames; i++) {\n held *= FADE_RATE;\n left[i] = held;\n if (right) { right[i] = held; }\n }\n this.lastSample = held;\n return true;\n }\n\n this.smoothFill += SMOOTH * (available - this.smoothFill);\n const fillError = this.smoothFill - TARGET_FILL;\n const speedAdj = Math.max(-MAX_ADJUST, Math.min(MAX_ADJUST, fillError * RATE_GAIN));\n let readCount = Math.round(frames * (1.0 + speedAdj));\n readCount = Math.max(1, Math.min(readCount, available));\n\n const buf = this.readBuf;\n for (let i = 0; i < readCount; i++) {\n buf[i] = this.ringView[(readPos + i) % RING_SIZE];\n }\n\n if (readCount === frames) {\n for (let i = 0; i < frames; i++) {\n const s = buf[i];\n left[i] = s;\n if (right) { right[i] = s; }\n }\n } else {\n const ratio = readCount / frames;\n for (let i = 0; i < frames; i++) {\n const srcPos = i * ratio;\n const idx = srcPos | 0;\n const frac = srcPos - idx;\n const s0 = buf[idx];\n const s1 = idx + 1 < readCount ? buf[idx + 1] : s0;\n const s = s0 + frac * (s1 - s0);\n left[i] = s;\n if (right) { right[i] = s; }\n }\n }\n\n this.lastSample = left[frames - 1];\n Atomics.store(this.headerView, 1, (readPos + readCount) % RING_SIZE);\n return true;\n }\n\n _processMp(left, right, frames) {\n const available = this.mpHead - this.mpTail;\n\n if (!this.mpPrimed) {\n if (available < PRE_BUFFER) {\n left.fill(0);\n if (right) { right.fill(0); }\n return true;\n }\n this.mpPrimed = true;\n }\n\n if (available === 0) {\n let held = this.lastSample;\n for (let i = 0; i < frames; i++) {\n held *= FADE_RATE;\n left[i] = held;\n if (right) { right[i] = held; }\n }\n this.lastSample = held;\n return true;\n }\n\n const toRead = Math.min(frames, available);\n for (let i = 0; i < toRead; i++) {\n const s = this.mpBuf[(this.mpTail + i) % MP_BUF_SIZE];\n left[i] = s;\n if (right) { right[i] = s; }\n }\n this.mpTail += toRead;\n\n if (toRead < frames) {\n let held = toRead > 0 ? left[toRead - 1] : this.lastSample;\n for (let i = toRead; i < frames; i++) {\n held *= FADE_RATE;\n left[i] = held;\n if (right) { right[i] = held; }\n }\n }\n\n this.lastSample = left[frames - 1];\n return true;\n }\n}\n\nregisterProcessor('nes-audio-processor', NESAudioProcessor);\n";
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export const audioWorkletSrc = `
|
|
2
|
+
const RING_SIZE = 4096;
|
|
3
|
+
const PRE_BUFFER = 1024;
|
|
4
|
+
const TARGET_FILL = 1536;
|
|
5
|
+
const SMOOTH = 0.005;
|
|
6
|
+
const RATE_GAIN = 0.000008;
|
|
7
|
+
const MAX_ADJUST = 0.005;
|
|
8
|
+
const FADE_RATE = 0.97;
|
|
9
|
+
const MP_BUF_SIZE = 16384;
|
|
10
|
+
|
|
11
|
+
class NESAudioProcessor extends AudioWorkletProcessor {
|
|
12
|
+
constructor() {
|
|
13
|
+
super();
|
|
14
|
+
this.headerView = null;
|
|
15
|
+
this.ringView = null;
|
|
16
|
+
this.ready = false;
|
|
17
|
+
this.primed = false;
|
|
18
|
+
this.lastSample = 0;
|
|
19
|
+
this.smoothFill = TARGET_FILL;
|
|
20
|
+
this.readBuf = new Float32Array(192);
|
|
21
|
+
this.mpBuf = new Float32Array(MP_BUF_SIZE);
|
|
22
|
+
this.mpHead = 0;
|
|
23
|
+
this.mpTail = 0;
|
|
24
|
+
this.mpMode = false;
|
|
25
|
+
this.mpPrimed = false;
|
|
26
|
+
|
|
27
|
+
this.port.onmessage = (e) => {
|
|
28
|
+
if (e.data.type === 'init') {
|
|
29
|
+
const sab = e.data.sharedBuffer;
|
|
30
|
+
this.headerView = new Int32Array(sab, 0, 2);
|
|
31
|
+
this.ringView = new Float32Array(sab, 8, RING_SIZE);
|
|
32
|
+
this.mpMode = false;
|
|
33
|
+
this.ready = true;
|
|
34
|
+
} else if (e.data.type === 'init-mp') {
|
|
35
|
+
this.mpMode = true;
|
|
36
|
+
this.ready = true;
|
|
37
|
+
} else if (e.data.type === 'push') {
|
|
38
|
+
const s = e.data.samples;
|
|
39
|
+
const avail = MP_BUF_SIZE - (this.mpHead - this.mpTail);
|
|
40
|
+
const count = Math.min(s.length, avail);
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
this.mpBuf[(this.mpHead + i) % MP_BUF_SIZE] = s[i];
|
|
43
|
+
}
|
|
44
|
+
this.mpHead += count;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process(inputs, outputs) {
|
|
50
|
+
const output = outputs[0];
|
|
51
|
+
if (!output || output.length === 0) { return true; }
|
|
52
|
+
|
|
53
|
+
const left = output[0];
|
|
54
|
+
const right = output.length > 1 ? output[1] : null;
|
|
55
|
+
const frames = left.length;
|
|
56
|
+
|
|
57
|
+
if (!this.ready) {
|
|
58
|
+
left.fill(0);
|
|
59
|
+
if (right) { right.fill(0); }
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (this.mpMode) {
|
|
64
|
+
return this._processMp(left, right, frames);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const writePos = Atomics.load(this.headerView, 0);
|
|
68
|
+
const readPos = Atomics.load(this.headerView, 1);
|
|
69
|
+
const available = (writePos - readPos + RING_SIZE) % RING_SIZE;
|
|
70
|
+
|
|
71
|
+
if (!this.primed) {
|
|
72
|
+
if (available < PRE_BUFFER) {
|
|
73
|
+
left.fill(0);
|
|
74
|
+
if (right) { right.fill(0); }
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
this.primed = true;
|
|
78
|
+
this.smoothFill = available;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (available === 0) {
|
|
82
|
+
let held = this.lastSample;
|
|
83
|
+
for (let i = 0; i < frames; i++) {
|
|
84
|
+
held *= FADE_RATE;
|
|
85
|
+
left[i] = held;
|
|
86
|
+
if (right) { right[i] = held; }
|
|
87
|
+
}
|
|
88
|
+
this.lastSample = held;
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.smoothFill += SMOOTH * (available - this.smoothFill);
|
|
93
|
+
const fillError = this.smoothFill - TARGET_FILL;
|
|
94
|
+
const speedAdj = Math.max(-MAX_ADJUST, Math.min(MAX_ADJUST, fillError * RATE_GAIN));
|
|
95
|
+
let readCount = Math.round(frames * (1.0 + speedAdj));
|
|
96
|
+
readCount = Math.max(1, Math.min(readCount, available));
|
|
97
|
+
|
|
98
|
+
const buf = this.readBuf;
|
|
99
|
+
for (let i = 0; i < readCount; i++) {
|
|
100
|
+
buf[i] = this.ringView[(readPos + i) % RING_SIZE];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (readCount === frames) {
|
|
104
|
+
for (let i = 0; i < frames; i++) {
|
|
105
|
+
const s = buf[i];
|
|
106
|
+
left[i] = s;
|
|
107
|
+
if (right) { right[i] = s; }
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
const ratio = readCount / frames;
|
|
111
|
+
for (let i = 0; i < frames; i++) {
|
|
112
|
+
const srcPos = i * ratio;
|
|
113
|
+
const idx = srcPos | 0;
|
|
114
|
+
const frac = srcPos - idx;
|
|
115
|
+
const s0 = buf[idx];
|
|
116
|
+
const s1 = idx + 1 < readCount ? buf[idx + 1] : s0;
|
|
117
|
+
const s = s0 + frac * (s1 - s0);
|
|
118
|
+
left[i] = s;
|
|
119
|
+
if (right) { right[i] = s; }
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.lastSample = left[frames - 1];
|
|
124
|
+
Atomics.store(this.headerView, 1, (readPos + readCount) % RING_SIZE);
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_processMp(left, right, frames) {
|
|
129
|
+
const available = this.mpHead - this.mpTail;
|
|
130
|
+
|
|
131
|
+
if (!this.mpPrimed) {
|
|
132
|
+
if (available < PRE_BUFFER) {
|
|
133
|
+
left.fill(0);
|
|
134
|
+
if (right) { right.fill(0); }
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
this.mpPrimed = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (available === 0) {
|
|
141
|
+
let held = this.lastSample;
|
|
142
|
+
for (let i = 0; i < frames; i++) {
|
|
143
|
+
held *= FADE_RATE;
|
|
144
|
+
left[i] = held;
|
|
145
|
+
if (right) { right[i] = held; }
|
|
146
|
+
}
|
|
147
|
+
this.lastSample = held;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const toRead = Math.min(frames, available);
|
|
152
|
+
for (let i = 0; i < toRead; i++) {
|
|
153
|
+
const s = this.mpBuf[(this.mpTail + i) % MP_BUF_SIZE];
|
|
154
|
+
left[i] = s;
|
|
155
|
+
if (right) { right[i] = s; }
|
|
156
|
+
}
|
|
157
|
+
this.mpTail += toRead;
|
|
158
|
+
|
|
159
|
+
if (toRead < frames) {
|
|
160
|
+
let held = toRead > 0 ? left[toRead - 1] : this.lastSample;
|
|
161
|
+
for (let i = toRead; i < frames; i++) {
|
|
162
|
+
held *= FADE_RATE;
|
|
163
|
+
left[i] = held;
|
|
164
|
+
if (right) { right[i] = held; }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
this.lastSample = left[frames - 1];
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
registerProcessor('nes-audio-processor', NESAudioProcessor);
|
|
174
|
+
`;
|