dewdrop-sdk 0.5.0 → 0.5.2
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 +4 -106
- package/package.json +1 -1
- package/src/index.js +3 -2
- package/src/session.js +24 -9
- package/src/voice.js +14 -71
package/README.md
CHANGED
|
@@ -1,112 +1,10 @@
|
|
|
1
1
|
# dewdrop-sdk
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
Build a three.js game for Dewdrop and run it locally: the Vite plugin, model
|
|
4
|
+
calls, multiplayer, saves and voice.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
In a Vite 6 or later project:
|
|
9
|
-
|
|
10
|
-
```sh
|
|
11
|
-
npm install -D dewdrop-sdk
|
|
12
|
-
npm install --save-exact three@0.185.1
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
```js
|
|
16
|
-
// vite.config.js
|
|
17
|
-
import dew from 'dewdrop-sdk/vite';
|
|
18
|
-
export default { plugins: [dew()] };
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
`npx dewctl init` sets up a new game this way, with the Dewdrop CLI beside it.
|
|
22
|
-
To update a project, run `npm install -D dewdrop-sdk@latest`; saves need 0.4.0
|
|
23
|
-
or later, voice 0.5.0.
|
|
24
|
-
|
|
25
|
-
## Saves locally
|
|
26
|
-
|
|
27
|
-
On Dewdrop, `localStorage` and IndexedDB aren't local: nothing your game
|
|
28
|
-
writes to them is stored in the player's browser or on their device, and they
|
|
29
|
-
start empty each time a player opens your game. `saves.save()` keeps what they
|
|
30
|
-
hold in Dewdrop's storage for that player, and `saves.load()` puts it back,
|
|
31
|
-
in later uploads of your game too. A game with a `backend.js` keeps no saves:
|
|
32
|
-
its state is its rooms', and both calls refuse with `NotSupportedError`.
|
|
33
|
-
`npm run dev` does the same in your browser: every page load starts with
|
|
34
|
-
`localStorage` and IndexedDB empty, and saves are kept in the browser, in a
|
|
35
|
-
database named `dewdrop-saves` your game doesn't see, for the game
|
|
36
|
-
`dewdrop.json` names and the player `?player=` names, across your edits. Tabs
|
|
37
|
-
of one dev server share the browser's storage, so loading one empties the
|
|
38
|
-
others'.
|
|
39
|
-
|
|
40
|
-
## Rooms locally
|
|
41
|
-
|
|
42
|
-
`npm run dev` runs your `backend.js` for each room the tabs join: `?room=`
|
|
43
|
-
names the room, `?player=` the player, and `?players=` how many players a
|
|
44
|
-
room the tab opens seats, 100 unless it says 1 to 100, as Dewdrop's rooms
|
|
45
|
-
do; `?players=1` is singleplayer. A player is counted once however many tabs
|
|
46
|
-
they play from, and a tab past a full room is refused, with the reason in
|
|
47
|
-
the console. In the backend, `clients.capacity` is the room's size.
|
|
48
|
-
|
|
49
|
-
## Sessions that end
|
|
50
|
-
|
|
51
|
-
On Dewdrop a session can end ahead of time: for maintenance, or a newer
|
|
52
|
-
version of your game published. Its player sees a countdown and reconnects
|
|
53
|
-
on their own, and your game hears of it first, up to fifteen minutes
|
|
54
|
-
before: `session.ending` from `dewdrop-sdk` is `{at, reason}`, `at` in
|
|
55
|
-
milliseconds as `Date.now()` counts and `reason` `'restart'` or `'update'`,
|
|
56
|
-
and an `ending` event fires each time it changes, its withdrawal to `null`
|
|
57
|
-
included. Call `saves.save()` then to keep what the player has done. `npm
|
|
58
|
-
run dev` ends no session ahead of time, so `session.ending` stays `null`
|
|
59
|
-
there.
|
|
60
|
-
|
|
61
|
-
## Voice
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
import { backend, voice } from 'dewdrop-sdk';
|
|
65
|
-
|
|
66
|
-
// This player's microphone, a chunk at a time, sent to the backend each frame.
|
|
67
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
68
|
-
const capture = voice.capture(stream); // capture.stop() to go quiet
|
|
69
|
-
function frame() {
|
|
70
|
-
for (const chunk of capture.read()) backend.send(chunk); // Int16Array, 800 samples at 48 kHz
|
|
71
|
-
requestAnimationFrame(frame);
|
|
72
|
-
}
|
|
73
|
-
requestAnimationFrame(frame);
|
|
74
|
-
|
|
75
|
-
// Another player's voice, as the backend sends it on: from their figure,
|
|
76
|
-
// loud up close, fading with distance.
|
|
77
|
-
const source = voice.source(listener.context);
|
|
78
|
-
const sound = new PositionalAudio(listener); // on their avatar
|
|
79
|
-
sound.setNodeSource(source.node); // or source.node.connect(anywhere)
|
|
80
|
-
backend.onmessage = ({ data }) => source.write(new Int16Array(data, 4)); // behind the backend's own header
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Dewdrop carries no voice between players itself: your `backend.js`
|
|
84
|
-
decides who hears whom — those within earshot, a team, everyone — and
|
|
85
|
-
sends each chunk on through its ordinary messages, so a game can mix,
|
|
86
|
-
mute, record or transcribe as it sees fit. `voice.capture(stream)` takes
|
|
87
|
-
the live microphone stream from `getUserMedia` and hands back the
|
|
88
|
-
capture: `read()` returns the chunks that arrived since the last read,
|
|
89
|
-
each an `Int16Array` of 800 samples, mono, at `voice.SAMPLE_RATE`
|
|
90
|
-
(48 kHz), and `stop()` ends it; one capture at a time, a second throws
|
|
91
|
-
an `InvalidStateError`. A read is at most eight chunks of 1,600 bytes,
|
|
92
|
-
which a client's frame sends to its backend whole within its 16 KiB
|
|
93
|
-
with room beside, so a game may send every chunk it reads; a backend's
|
|
94
|
-
frame sends some 160 copies on within its 256 KiB, so a crowd all
|
|
95
|
-
speaking at once is more than one frame carries and a backend sheds the
|
|
96
|
-
rest — voice is late or nothing — or compresses first. `voice.source(context)`
|
|
97
|
-
is plain Web Audio: `node` is a node of `context` for `connect` or a
|
|
98
|
-
three.js `PositionalAudio`'s `setNodeSource`, `write(samples)` plays an
|
|
99
|
-
`Int16Array` or `Float32Array` right after those written before it —
|
|
100
|
-
the first two chunks ahead of the clock, so a chunk a little early or
|
|
101
|
-
late plays without a gap — dropping what runs eight chunks ahead, and
|
|
102
|
-
`close()` leaves the graph. `npm run dev` reads your browser's
|
|
103
|
-
microphone through an audio worklet and hands your game the same chunks,
|
|
104
|
-
so voice works between tabs locally as it does on Dewdrop.
|
|
105
|
-
|
|
106
|
-
## Docs
|
|
107
|
-
|
|
108
|
-
How to build your game, from the look to model calls, multiplayer, saves and
|
|
109
|
-
what works on Dewdrop, is in the docs: https://alpha.dewdrop.world/docs
|
|
6
|
+
How to set up a project and everything the SDK does is in the docs:
|
|
7
|
+
https://alpha.dewdrop.world/docs
|
|
110
8
|
|
|
111
9
|
## License
|
|
112
10
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
// runs, `clients`, in that backend, over its port back, and `saves` into
|
|
6
6
|
// the browser's own storage, which the plugin's script empties as each
|
|
7
7
|
// page loads; `session` says when a session ends ahead of time, which here
|
|
8
|
-
// none does
|
|
9
|
-
//
|
|
8
|
+
// none does, and lets a game end its player's session, which here ends
|
|
9
|
+
// nothing; and `voice` reads the microphone through the browser's own
|
|
10
|
+
// audio worklet, into the chunks a game gets on Dewdrop.
|
|
10
11
|
export { llm } from './llm.js';
|
|
11
12
|
export { backend } from './backend.js';
|
|
12
13
|
export { clients } from './clients.js';
|
package/src/session.js
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
// `session` on a developer's machine. On Dewdrop a session can end ahead of
|
|
2
|
-
// time — for maintenance,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// and
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
2
|
+
// time — for maintenance, a newer version of its game published, its player
|
|
3
|
+
// idle for fifteen minutes, its player away for ten — the page hidden, or
|
|
4
|
+
// another game of theirs showing — or its player choosing to play in
|
|
5
|
+
// another tab — and hears of it first, up to fifteen minutes before, a
|
|
6
|
+
// minute for an idle or an away player, and as it ends for a player playing
|
|
7
|
+
// elsewhere: `session.ending` is `{at, reason}`, `at` in milliseconds since
|
|
8
|
+
// the epoch as `Date.now()` counts and `reason` `'restart'`, `'update'`,
|
|
9
|
+
// `'idle'`, `'away'`, `'elsewhere'` or `'exit'`, and an `ending` event fires
|
|
10
|
+
// each time it changes, its withdrawal to null included, so a game can save
|
|
11
|
+
// and say so. A game ends its player's session itself with
|
|
12
|
+
// `session.exit()`, which on Dewdrop takes the player back to Dewdrop's
|
|
13
|
+
// home page while the room's other players play on, the game hearing
|
|
14
|
+
// `'exit'` as it goes; a backend has no player to send anywhere, so there it
|
|
15
|
+
// throws NotSupportedError. `npm run dev` ends no session, so here `ending`
|
|
16
|
+
// stays null and the event never fires, a game trying its handler by
|
|
17
|
+
// calling it, and `exit()` ends nothing: the console says so, once.
|
|
18
|
+
import { defineHandlers, side } from './net.js';
|
|
11
19
|
|
|
20
|
+
let told = false;
|
|
12
21
|
class Session extends EventTarget {
|
|
13
22
|
get ending() { return null; }
|
|
23
|
+
exit() {
|
|
24
|
+
if (side.backend) throw new DOMException('session.exit is unavailable in a backend', 'NotSupportedError');
|
|
25
|
+
if (told) return;
|
|
26
|
+
told = true;
|
|
27
|
+
console.info('[dewdrop] session.exit(): on Dewdrop this ends the session and takes the player back to Dewdrop’s home page; npm run dev keeps the game running');
|
|
28
|
+
}
|
|
14
29
|
}
|
|
15
30
|
defineHandlers(Session.prototype, ['ending']);
|
|
16
31
|
|
package/src/voice.js
CHANGED
|
@@ -1,29 +1,18 @@
|
|
|
1
|
-
// `voice` on a developer's machine: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// game captures in `npm run dev` as it captures on Dewdrop. `source` is
|
|
10
|
-
// plain Web Audio, the same function as Dewdrop's, and plays here as
|
|
11
|
-
// there.
|
|
1
|
+
// `voice` on a developer's machine: the player's microphone a chunk at a
|
|
2
|
+
// time, as a game gets it on Dewdrop, where Dewdrop carries no voice
|
|
3
|
+
// between players and plays none itself: a game sends what it captures
|
|
4
|
+
// to its backend through its messages, the backend sends it on to the
|
|
5
|
+
// players it chooses, and their games play it through their own audio
|
|
6
|
+
// graphs. Here the browser's own audio worklet reads the microphone and
|
|
7
|
+
// hands the game the same chunks, so a game captures in `npm run dev` as
|
|
8
|
+
// it captures on Dewdrop.
|
|
12
9
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// reads late, the newest kept.
|
|
10
|
+
// The chunks as Dewdrop hands them to a game: the rate of their samples,
|
|
11
|
+
// the samples in one chunk, 1/60 s, and the chunks that wait for a game
|
|
12
|
+
// that reads late, the newest kept.
|
|
17
13
|
export const SAMPLE_RATE = 48000;
|
|
18
14
|
export const VOICE_CHUNK_SAMPLES = 800;
|
|
19
15
|
export const VOICE_CHUNKS_HELD = 8;
|
|
20
|
-
// How far ahead of the context's clock a source begins what is written
|
|
21
|
-
// to it — the first chunk, and the first after it ran dry — in seconds:
|
|
22
|
-
// two chunks, which a later chunk may come late by and still follow the
|
|
23
|
-
// last without a gap; and how far ahead writes may run before the newest
|
|
24
|
-
// is dropped instead, so a voice falls no further behind.
|
|
25
|
-
const SOURCE_LEAD = 2 * VOICE_CHUNK_SAMPLES / SAMPLE_RATE;
|
|
26
|
-
const MAX_SOURCE_LEAD = 8 * VOICE_CHUNK_SAMPLES / SAMPLE_RATE;
|
|
27
16
|
|
|
28
17
|
// The processor that reads the microphone in the browser's audio thread:
|
|
29
18
|
// it takes the microphone's frames at the context's rate, steps through
|
|
@@ -141,52 +130,6 @@ function capture(stream) {
|
|
|
141
130
|
return mine;
|
|
142
131
|
}
|
|
143
132
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// they come — Int16Array or Float32Array samples at SAMPLE_RATE, mono,
|
|
148
|
-
// as `capture` reads them or as another player's reached this game —
|
|
149
|
-
// each right after the last: `node` is the source, for `connect`
|
|
150
|
-
// or a three.js PositionalAudio's `setNodeSource`; `write` plays a chunk
|
|
151
|
-
// after those before it; `close` leaves the graph. Plain Web Audio — a
|
|
152
|
-
// buffer source a write, begun SOURCE_LEAD ahead of the context's clock
|
|
153
|
-
// and following the last, and nothing of Dewdrop's — so a game may play a
|
|
154
|
-
// voice its own way instead. The runtime's web/voice.js carries this same
|
|
155
|
-
// function, which tests/sync.test.mjs pins line for line.
|
|
156
|
-
function source(context) {
|
|
157
|
-
if (!isObjectLike(context) || typeof context.createBufferSource !== 'function') {
|
|
158
|
-
throw new TypeError('voice.source: context is not an AudioContext');
|
|
159
|
-
}
|
|
160
|
-
const node = context.createGain();
|
|
161
|
-
// When the next chunk written plays, on the context's clock.
|
|
162
|
-
let at = 0;
|
|
163
|
-
return Object.freeze({
|
|
164
|
-
node,
|
|
165
|
-
write(samples) {
|
|
166
|
-
const tag = Object.prototype.toString.call(samples);
|
|
167
|
-
if (tag !== '[object Int16Array]' && tag !== '[object Float32Array]') {
|
|
168
|
-
throw new TypeError('voice.source: write takes an Int16Array or a Float32Array of samples');
|
|
169
|
-
}
|
|
170
|
-
if (samples.length === 0) return;
|
|
171
|
-
const now = context.currentTime;
|
|
172
|
-
// The first write, or one after the chunks before it played out:
|
|
173
|
-
// SOURCE_LEAD ahead of the clock. While they still play, right
|
|
174
|
-
// after them, however early or late this one came, which is what
|
|
175
|
-
// rides out the jitter; too far ahead of the clock: dropped.
|
|
176
|
-
if (at <= now) at = now + SOURCE_LEAD;
|
|
177
|
-
else if (at > now + MAX_SOURCE_LEAD) return;
|
|
178
|
-
const buffer = context.createBuffer(1, samples.length, SAMPLE_RATE);
|
|
179
|
-
const channel = buffer.getChannelData(0);
|
|
180
|
-
if (tag === '[object Float32Array]') channel.set(samples);
|
|
181
|
-
else for (let i = 0; i < samples.length; i++) channel[i] = samples[i] / 32768;
|
|
182
|
-
const player = context.createBufferSource();
|
|
183
|
-
player.buffer = buffer;
|
|
184
|
-
player.connect(node);
|
|
185
|
-
player.start(at);
|
|
186
|
-
at += buffer.duration;
|
|
187
|
-
},
|
|
188
|
-
close() { node.disconnect(); },
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export const voice = Object.freeze({ capture, source, SAMPLE_RATE });
|
|
133
|
+
// SAMPLE_RATE is the chunks' rate, for the buffers a game plays them
|
|
134
|
+
// through.
|
|
135
|
+
export const voice = Object.freeze({ capture, SAMPLE_RATE });
|