A game that stays silent is only half a game, so the pages say what a sound is (#45)
1 parent
84df59c535
commit
452b31df54
8 files changed
+144
-19
No files matched your search
+15
-4
@@ -13,6 +13,9 @@ All routes go through `api/index.php?r=…` (the site's nginx does not pass `PAT
|
||||
| GET | `fonts` | `[{file,name,bytes,updatedAt}]` — what the font picker offers |
|
||||
| POST | `fonts&name=Serif.ttf` | **raw font bytes** → `{file,name,bytes}` (201) |
|
||||
| DELETE | `fonts/{file}` | 204 |
|
||||
| GET | `sounds` | `[{file,name,bytes,updatedAt}]` — what the sound picker offers |
|
||||
| POST | `sounds&name=jump.wav` | **raw sound bytes** → `{file,name,bytes}` (201) |
|
||||
| DELETE | `sounds/{file}` | 204 |
|
||||
|
||||
Every project document carries `formatVersion` (`PROJECT_FORMAT_VERSION` in `core/model/Project.ts`,
|
||||
currently 2). There are no load-time migrations: `serialize.ts` reads what it recognises and drops
|
||||
@@ -20,10 +23,18 @@ the rest.
|
||||
|
||||
Uploads send the bytes as the body rather than `multipart/form-data`: no boundary parsing in
|
||||
PHP, and PHP's `upload_max_filesize` (2 MB) never applies — only `post_max_size`. The API has its
|
||||
own caps behind nginx's: 8 MB per request, 4 MB per image or font (413). The stored
|
||||
files are served statically from `data/textures/` and `data/fonts/`, so reading one never runs
|
||||
PHP. An image is downscaled and re-encoded in the browser when it would not fit; a font cannot
|
||||
be, so an oversized typeface is refused rather than shrunk.
|
||||
own caps behind nginx's: 20 MB per request, 4 MB per image or font and 16 MB per sound (413) —
|
||||
a single music track is worth more than four typefaces. **nginx's own limit is the one that
|
||||
actually bites** (1 MB today, see `MAX_UPLOAD_BYTES` in `textureImport.ts`): raising the API's
|
||||
cap without raising that one changes nothing. The stored files are served statically from
|
||||
`data/textures/`, `data/fonts/` and `data/sounds/`, so reading one never runs PHP. An image is
|
||||
downscaled and re-encoded in the browser when it would not fit; a font or a sound cannot be, so
|
||||
an oversized one is refused rather than shrunk.
|
||||
|
||||
The format of a font or a sound is read from the file's **own signature**, never from the
|
||||
`Content-Type` of the request: browsers name audio inconsistently (`audio/mp4`, `audio/x-m4a`,
|
||||
nothing at all for the same `.m4a`). MP3 is the one with no single signature — either an `ID3`
|
||||
tag or a frame header of eleven set bits, which is what `SoundStore::mp3Extension` demands.
|
||||
|
||||
**`name` is the original file name**, kept beside the stored file as `{file}.json` and returned
|
||||
with it. The stored file is always `{sha256}.{ext}`, so the name is a caption and never a path:
|
||||
|
||||
+23
-6
@@ -55,17 +55,18 @@ src/core/graph/ graph model, NodeRegistry/NodeContext/values, interpreter
|
||||
src/core/particles/ nodes (the catalogue), compile (a graph → one function per output),
|
||||
emission (which particles are alive at a second — a pure rule, no state)
|
||||
src/core/store/ serialize (untrusted JSON → Project), http (one API call), ProjectStore/HttpProjectStore,
|
||||
TextureStore/HttpTextureStore, FontStore/HttpFontStore
|
||||
TextureStore/HttpTextureStore, FontStore/HttpFontStore, SoundStore/HttpSoundStore
|
||||
src/core/time.ts the global clock: one scaled step a frame, shared by the world, the particles, the shaders and the animation preview
|
||||
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem
|
||||
src/runtime/ World, Entity, System interface, Input tracker, CollisionSystem, AnimationSystem, BehaviorSystem,
|
||||
Audio (the AudioSink a run plays into — the counterpart of Input on the way out)
|
||||
src/ui/ widget toolkit (generic, knows nothing about assets)
|
||||
src/viewport/ view (transforms), items (what to draw), symbols (how to draw it),
|
||||
TextureLibrary, FontLibrary, ShaderLibrary, ParticleLibrary,
|
||||
TextureLibrary, FontLibrary, SoundLibrary, ShaderLibrary, ParticleLibrary,
|
||||
particles (one ParticleContainer per source), activeCamera — no editor concepts,
|
||||
so tests reach it freely
|
||||
src/editor/ App → Editor (layout + ticker), PlaySession (the run), ProjectSession (load/create),
|
||||
EditorRun (run in editor), state, actions, shortcuts, debugHook, autosave,
|
||||
fileImport (drop/paste) → textureImport / fontImport,
|
||||
fileImport (drop/paste) → textureImport / fontImport / soundImport,
|
||||
framing (fit/marquee/snap maths), AssetTabEditor (the shared shell of the asset tabs),
|
||||
TabStrip + tabRow (the tab row and what it does when the tabs outgrow it)
|
||||
src/editor/viewport/ WorldViewport base + SceneViewport/GameViewport — editing tools, hence not in src/viewport/
|
||||
@@ -85,8 +86,8 @@ src/editor/particles/ ParticlesEditor (graph canvas left of a preview panel, sp
|
||||
ParticlePreview (the panel: the asset's own particles, wearing its own rows)
|
||||
src/editor/list/ ListEditor (a row per item), slot (which list is being edited, and how it is written back)
|
||||
src/editor/console/ log (ring buffer, argument formatting, the console.* capture), ConsolePanel (the tail under the tabs)
|
||||
api/ PHP backend: index.php (routing), lib/{Store,TextureStore,FontStore,OriginalName,Response}.php
|
||||
data/projects/ persisted projects, data/textures/ + data/fonts/ content-addressed uploads
|
||||
api/ PHP backend: index.php (routing), lib/{Store,TextureStore,FontStore,SoundStore,OriginalName,Response}.php
|
||||
data/projects/ persisted projects, data/textures/ + data/fonts/ + data/sounds/ content-addressed uploads
|
||||
tests/ Vitest unit tests tools/ seed-*, e2e (steps in tools/e2e/, rig in tools/e2e/lib/), smoke-*, lib/, assets/ (the walk demo's sheet)
|
||||
```
|
||||
|
||||
@@ -144,6 +145,22 @@ step from there**; a second clock beside it is the thing to avoid, not to add. N
|
||||
the other way round: **whoever keeps an absolute second of a clock has to survive that clock going
|
||||
back to zero**, which it does at both ends of a run. A particle field keeps its origin that way, so
|
||||
one in the future means the clock was pulled out from under it and it starts over.
|
||||
**The time scale is deliberately not part of one of them: pitch.** A game in slow motion keeps
|
||||
its sounds at normal speed — a tape being slowed is a sound effect of its own, not the meaning of
|
||||
"half speed" — so the audio device is left alone and only what the world *does* is scaled.
|
||||
A pause is the exception, because a voice playing over a still image is nobody's idea of paused:
|
||||
the audio context is suspended and resumes mid-note.
|
||||
|
||||
**Everything the graph does to the outside world goes through the context, audio included.**
|
||||
`NodeContext.playSound` takes an asset id, not a file: resolving it — and folding the asset's own
|
||||
volume and loop setting together with the node's (`loop` is `boolean | undefined`, and `undefined`
|
||||
is what leaves the answer to the asset) — is the `BehaviorSystem`'s job, since it is the one
|
||||
holding the project. Below that line, `AudioSink` (`runtime/systems/Audio.ts`) knows only
|
||||
files and voices, and `viewport/SoundLibrary` implements it. That keeps `core/` free of the
|
||||
browser the way it is free of Pixi, lets a test run a world that plays into a recorder instead of
|
||||
a device, and is the reason a run started before anything wired a sink in plays silently rather
|
||||
than crashing.
|
||||
|
||||
**A predicate named after what a thing looks like will be asked what a thing *is*.** `isSymbolType`
|
||||
reads as "draws an icon", but four callers use it for "is editor decoration": the running world, the
|
||||
Game tab, the draw order and the upright placement all key off it. Giving a particle source an icon
|
||||
|
||||
+5
-2
@@ -99,7 +99,7 @@ shown on the node and editable in the inspector.
|
||||
- **Node set** (`src/core/graph/nodes/`, one file per node): events `On start`, `On enable`, `On update`, `On disable`, `On destroy` (see *Lifecycle*), `On key down/up/held`,
|
||||
`On timer`, `On collision enter/stay/exit`, `On click` (the mouse pressed on the owner — anywhere when it has none), `On animation finished`, `On state entered`, `On state exited` (one output per state, see *State machines*), `Start` · flow `Branch`, `Sequence`, `Repeat`, `Run behavior`, `End`, and the three that move or follow a state machine, `State branch`, `Set state`, `State transition` (see *State machines*) · data `Self`, `Owner`, `Parent`, `Object by name`, `Name of`,
|
||||
`Is asset` (is this object an instance of asset X — how you tell what you collided with),
|
||||
`Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`, `Add/Subtract/Multiply/Divide`,
|
||||
`Variables`, `Properties` (every property row of the target's type, the transform rows `scale`, `rotation`, `mirrorX`, `mirrorY` included — how a graph turns a sprite), `Get position` (local and world), `Is enabled`, literals `Number`/`Text`/`Boolean`/`Texture`/`State machine`/`Sound`, `Add/Subtract/Multiply/Divide`,
|
||||
`Min/Max/Abs/Sqrt/Clamp`, `Floor/Ceil/Round`, `Modulo`, `Random`, `Time`, `To text`, `To number`, `Join text`,
|
||||
`Substring`, `Find text`, `Equals`,
|
||||
lists `List` (a constant), `Fill` (one value repeated — how an empty board is made), `Count`,
|
||||
@@ -109,7 +109,10 @@ shown on the node and editable in the inspector.
|
||||
normalized direction, so diagonals are not faster), `Compare`, `And/Or/Not`, `Key is down`,
|
||||
`Mouse position`, `Mouse button`, `Is hovered`, `Is touching`, `Current state` (name and id), `Is in state` (one boolean per state) ·
|
||||
actions `Move` (dx, dy, scale, dt — connect an event's dt for per-second movement; in the object's own axes, which a turned parent turns too), `Set position`, `Spawn`
|
||||
(asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*), `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef`
|
||||
(asset setting + x/y/z, optional parent → outputs the new object), `Destroy`, `Emit` (starts a particle source over, see *Particles*),
|
||||
`Play sound` / `Stop sound` (see [Sounds](Sounds): the asset says how loud and whether it loops, the node's
|
||||
`volume` multiplies that and its `loop` either leaves it alone or overrules it in either
|
||||
direction; every play is a voice of its own, so two in a row overlap), `Log`, `Stop game`, `Restart game`. A node type is one file exporting a `NodeDef`
|
||||
(ports, settings, `run`), listed in `nodes/index.ts`; palette, ports, validation and
|
||||
inspector fields are generated from it. A port may declare a `default` for its inline value
|
||||
(e.g. `Move.scale = 1`), shown on the node and in the inspector.
|
||||
|
||||
+3
-2
@@ -1,8 +1,9 @@
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`, `font`, `particles`, `variables`) and typed properties. Assets have **no position**. |
|
||||
| **Texture** | An uploaded image. Scenes, textures, animations, state machines, shaders, fonts and Variables assets are the seven types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
|
||||
| **Asset** | Named thing with a type (`scene`, `camera`, `sprite`, `text`, `behavior`, `collider`, `container`, `texture`, `animation`, `statemachine`, `shader`, `font`, `sound`, `particles`, `variables`) and typed properties. Assets have **no position**. |
|
||||
| **Texture** | An uploaded image. Scenes, textures, animations, state machines, shaders, fonts, sounds and Variables assets are the eight types that cannot be placed: a texture has no position, it is *worn* by a sprite through the ordinary property `texture`. See [Textures](Textures). |
|
||||
| **Font** | An uploaded typeface, worn by a text through the ordinary property `font`; no font means the editor's own. Uploaded and picked exactly like a texture, and stored the same way. See [Fonts](Fonts). |
|
||||
| **Sound** | An uploaded audio file. Uploaded, picked and stored exactly like a typeface — but nothing *wears* it: it is neither on screen nor in the scene, and a graph plays it with `Play sound`. Its own rows say how loud it is and whether it loops; the node scales the one and can overrule the other either way. See [Sounds](Sounds). |
|
||||
| **State machine** | A named set of **states**, edited as nodes in its own tab. A behavior puts it into a state with `Set state`, moves it along a named transition with `State transition` and branches on it with `State branch`. Its **Scope** says where the current state lives: **Local** gives every behavior instance one of its own, **Global** keeps a single one for the whole run. See [State machines](State-machines). |
|
||||
| **Shader** | A node graph that becomes the GLSL a sprite is drawn with, edited in its own tab. A sprite wears it through `shader`; the numbers the graph declares are rows below that dropdown. See [Shaders](Shaders). |
|
||||
| **Particles** | A node graph that says what *one* particle is as a function of its age and of a random number drawn once for it — no forces, no simulation. Unlike the other graph assets it **is** placeable: the object made from it is a source, whose rows say how many go out and for how long they live. Because a particle is a formula, the same second always draws the same picture and the tab's slider can jump to any of them exactly. See [Particles](Particles). |
|
||||
|
||||
+13
-4
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
|
||||
# code is lost (tail's is reported), so a red run reads as green
|
||||
npm run build # writes dist/ and regenerates index.html
|
||||
./build.sh # the same under `set -o pipefail`, for callers that pipe the output
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 885 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
node tools/seed-e2e.mjs && node tools/e2e.mjs # 901 editor checks, screenshots in /tmp/gm-e2e-*.png
|
||||
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
|
||||
./e2e.sh start [GM_STEPS] # the same run detached, see below
|
||||
./e2e.sh status # live progress, or the last run's result
|
||||
@@ -64,7 +64,7 @@ The e2e run is a list of steps in `tools/e2e/` (hierarchy, inspector, play, asse
|
||||
graph, parenting, colliders, containers, text/enabled, menus, textures, frames, overflow, aspect,
|
||||
container tab, run-in-editor, pause, nested behaviors, key repeat, list tab, struct variable,
|
||||
variable nodes, property nodes, animations, state machines, console, shaders, transforms, fonts,
|
||||
particles, shared scope, render on change) that build on each other's
|
||||
particles, shared scope, render on change, sounds) that build on each other's
|
||||
scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `undo`, `play`,
|
||||
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
|
||||
exercises.
|
||||
@@ -78,7 +78,7 @@ The same holds for the **`+` menu**: it has one entry per asset type and scrolls
|
||||
last of them sit under the mask, where `locate` still finds an entry but a click on it lands on
|
||||
nothing. A step picks a type with `pickAssetType(t, 'Font')` from the same file, which scrolls it in
|
||||
first. The fourteenth type (Variables) pushed `Font` under that edge and took the font step down,
|
||||
because only the particles step had carried the workaround of its own.
|
||||
because only the particles step had carried the workaround of its own. Sound is the fifteenth.
|
||||
|
||||
**What no colour check can see: whether anything was drawn at all.** `__gm.pixelAt` re-renders the
|
||||
stage on demand, so it answers correctly on a screen that has been frozen for a minute — which is
|
||||
@@ -91,6 +91,15 @@ tab whose picture stands still — the scene viewport may be wearing a Time shad
|
||||
and animation previews are moving pictures by definition. Screenshots ask for a frame first
|
||||
(`__gm.wake()` in `harness.mjs`) rather than trusting what the compositor still holds.
|
||||
|
||||
**Nothing is ever heard, and that is fine.** The browser runs with `--mute-audio`, plus
|
||||
`--autoplay-policy=no-user-gesture-required` so a voice still *starts* — otherwise the sound
|
||||
checks would be measuring a browser that refuses to play rather than an editor that never asked
|
||||
it to. `__gm.sound.stats()` (voices started, stopped, in flight, and the last one with its volume
|
||||
and loop flag) is the witness, for the reason `renderStats` is one. The seed's two sounds are
|
||||
**generated WAVs** (`tools/lib/audio.mjs`), not files: unlike the DejaVu typefaces there is no
|
||||
audio file the host and the CI image are guaranteed to share, and the bytes are a pure function of
|
||||
the arguments, so storage stays content-addressed and a check can name a file by its exact size.
|
||||
|
||||
**A check that demands an exact number must be handed the number, not something it can be derived
|
||||
from.** The particle preview used to hold second 0.5 by computing the clock reading that *would* mean
|
||||
0.5 since the field started; the round trip through floating point misses by the last bit about one
|
||||
@@ -183,4 +192,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
|
||||
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
|
||||
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
|
||||
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
|
||||
WebGL 2.0, and the same 885 checks as here.
|
||||
WebGL 2.0, and the same 901 checks as here.
|
||||
+3
-1
@@ -15,6 +15,7 @@ Live: <https://nockal.com/game-maker/> · Widget playground: <https://nockal.com
|
||||
| [Colliders](Colliders) | Collider assets, collision events, blocking movement |
|
||||
| [Textures](Textures) | Getting images in, where they go, tinting, the filter, frames, upload limits |
|
||||
| [Fonts](Fonts) | Uploading a typeface, what a text wears, what the editor falls back to |
|
||||
| [Sounds](Sounds) | Uploading a sound, what the asset says, `Play sound` and `Stop sound`, what a pause and a stop do to a voice |
|
||||
| [Animations](Animations) | Animation assets, the Animation tab, how they run |
|
||||
| [State machines](State-machines) | The state machine asset, its scope, named transitions, the start state, `State branch`, `Set state`, `State transition`, the two state events and the live highlight |
|
||||
| [Shaders](Shaders) | The shader asset, the Shader tab, the node set, the Adjust colors recipe, wearing a shader |
|
||||
@@ -39,7 +40,8 @@ into issues #1-#50, closed ones included, each carrying the commits it was filed
|
||||
## Not yet
|
||||
|
||||
Several scenes in one run (a `Load scene` node — screens are containers
|
||||
today), sound, auth, touch-optimised layout, graph comments/groups. A frame is always stretched to
|
||||
today), auth, touch-optimised layout, graph comments/groups. Sound has no mixer, no fade and no
|
||||
`Set volume` on a playing voice (see [Sounds](Sounds)). A frame is always stretched to
|
||||
the sprite's `width` x `height`: no nine-slice. Animations have no state machine, no restart node
|
||||
and no preview in the Scene tab. Slicing cuts a regular grid only — no detection of where the sprites actually
|
||||
sit on the sheet. Unreferenced image files are never collected — deliberately, since the image
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
A **sound asset** is an uploaded audio file. Nothing wears it and nothing places it — it is
|
||||
neither on screen nor in the scene tree. A graph plays it, with **Play sound**, and that is the
|
||||
only way a project makes a noise.
|
||||
|
||||
**Getting a sound in.** **Dropping an `.mp3`, `.ogg`, `.wav` or `.m4a` file onto the canvas**
|
||||
uploads it and makes the asset in one go; several at once are imported one after another, and a
|
||||
file that fails does not stop the rest. The asset is named after the file ("jump.wav" becomes
|
||||
"jump"). A dropped file is recognised by its **extension**, not by its MIME type: browsers report
|
||||
an `.m4a` as `audio/mp4`, `audio/x-m4a` or as nothing at all.
|
||||
|
||||
**+ → Sound** asks the other question and opens the picker: **every sound the server already
|
||||
holds**, one row each. A sound has nothing to show, so the row carries a **▶** at its left that
|
||||
plays it — pressing that is a preview and picks nothing. Picking a row and pressing **Use sound**
|
||||
makes the asset; **Upload…** is the way in for a file the server does not have yet. A row
|
||||
captioned with an asset name is one this project already uses, and picking it uses that asset
|
||||
rather than making a second one.
|
||||
|
||||
**Where it goes.** The bytes are uploaded to `POST ?r=sounds` and stored as
|
||||
`data/sounds/{sha256}.{ext}` — named after their own content, exactly like a typeface, and with
|
||||
the same four consequences (one file per sound however often it is uploaded, cacheable forever, a
|
||||
short string in the project document, an undoable delete). A sound is **never re-encoded on the
|
||||
way**: what the server stores is the file that was picked. The format is read from the file's own
|
||||
signature, so nothing a request claims can decide what is written.
|
||||
|
||||
**What the asset says.** Its inspector has two rows and a preview:
|
||||
|
||||
| Row | Meaning |
|
||||
| --- | --- |
|
||||
| **Volume** | How loud this sound is by itself, 0–1. |
|
||||
| **Loop** | Whether it is music: it repeats until something stops it. |
|
||||
|
||||
The preview plays through the very same library a running game uses — same decode, same device —
|
||||
so what the inspector plays is what the graph will play.
|
||||
|
||||
## Playing it
|
||||
|
||||
| Node | What it does |
|
||||
| --- | --- |
|
||||
| **Play sound** | Starts the sound named by its `sound` input. `volume` **multiplies** the asset's own volume (1 leaves it alone) and `loop` is a choice of three: *From the asset*, *On*, *Off*. |
|
||||
| **Stop sound** | Silences every voice of the sound it names, however many plays started it — a loop included. Naming no sound silences everything the run is playing. |
|
||||
| **Sound** | A constant sound, for either of the two to work on through an edge. |
|
||||
|
||||
The division is deliberate: the **asset says what a sound is**, the node only colours it. A
|
||||
`Play sound` with nothing typed into it therefore plays the sound the way the project describes
|
||||
it, and turning every jump sound down is one edit on one asset rather than a hunt through the
|
||||
graphs.
|
||||
|
||||
**Why `loop` has three answers and not a checkbox.** A tick box can only say *yes* or *no*, and
|
||||
one of the two has to double as "nothing said" — so whichever way it is read, one case is lost.
|
||||
It used to add to the asset's flag, which made music impossible to play one-shot. The choice is
|
||||
therefore *From the asset* (the default: the node says nothing and the asset decides), *On* and
|
||||
*Off*, and the last two overrule the asset in their direction. A drum loop can be used as a single
|
||||
hit, and an effect can be looped, without a second asset for either.
|
||||
|
||||
An edge may drive the input instead; a boolean arriving there is taken literally, so a comparison
|
||||
or a variable wired into `loop` reads as *On* / *Off*.
|
||||
|
||||
**Every play is a voice of its own.** Firing `Play sound` twice in a row is heard twice,
|
||||
overlapping — two coins picked up half a second apart sound like two coins. This holds for looping
|
||||
sounds as well: playing a loop twice gives two loops, and it is `Stop sound` that ends all of
|
||||
them at once.
|
||||
|
||||
**It arrives late.** The bytes are fetched and decoded on first use, so the very first play of a
|
||||
sound starts a moment after the node ran. `Play sound` never waits on the network, and a
|
||||
`Stop sound` that happens while the bytes are still in flight wins: the sound does not start
|
||||
afterwards on its own.
|
||||
|
||||
**The run owns its sounds.** Pausing (`Ctrl+Enter`) holds every voice where it is and resuming
|
||||
carries it on mid-note. Stopping the run (`Esc`) ends every voice, looping music included —
|
||||
nothing may outlive the world that started it. An editor run does the same, and is restarted by
|
||||
any edit, so a loop cannot pile up one voice per keystroke.
|
||||
|
||||
**Time scale does not change the pitch.** A game played in slow motion keeps its sounds at
|
||||
normal speed rather than sounding like a tape being slowed — the global clock scales what the
|
||||
world does, not how the device plays back.
|
||||
|
||||
**Known limits.** There is no `Set volume` on a playing voice, no `Is playing`, no fade and no
|
||||
positional audio: a sound is as loud as the asset and the node together say, from the moment it
|
||||
starts. There is no mixer and no master volume — the browser's own is the one. And nothing
|
||||
collects sound files a project stopped referencing; they stay on the server, which is what makes
|
||||
deleting a sound asset undoable.
|
||||
+1
@@ -6,6 +6,7 @@
|
||||
- [Colliders](Colliders)
|
||||
- [Textures](Textures)
|
||||
- [Fonts](Fonts)
|
||||
- [Sounds](Sounds)
|
||||
- [Animations](Animations)
|
||||
- [State machines](State-machines)
|
||||
- [Shaders](Shaders)
|
||||
|
||||
Reference in new issue
Block a user