redguard-preservation 0.10.2

A CLI tool to preserve Redguard files
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
# Positional Sound

How sound effects from [MAIN.SFX](../formats/SFX.md) and speech from
[RTX](../formats/RTX.md) get their volume, stereo pan and playback rate.

## Units

- Engine positions are stored in world units × 256 (eight fractional bits).
  World units are the units `SYSTEM.INI` distances use.
- Angles use 2048 units per turn, wrapped modulo 2048.
- Volumes and pans are Miles Sound System sample values: volume 0–127, pan
  0–127 with 64 at the centre (Miles: 0 = left, 127 = right).

## Listener

The listener is the render camera, not the player: its position is the view
position and its heading is the view yaw. In third person, distances are
therefore measured from behind Cyrus. Only the yaw enters the calculation;
camera pitch and roll do not.

## Radius and divisor

`SYSTEM.INI [system] sound_distance` (shipped `64`) is read once at startup
into two values, which the engine's debug sound page labels "Sound radius" and
"Sound divisor":

| Value | Formula | Shipped |
|---|---|---|
| Radius `R` | `sound_distance × 36` world units | 2304 |
| Divisor `D` | `floor(65536 / floor(R / 128))` | 3640 |

A `sound_distance` of 3 or less makes `floor(R / 128)` zero, a division by
zero in the original; such values are unsupported.

## Distance

For emitter `E` and listener `L` in engine units (world units × 256):

```
d = floor(round(sqrt(dx² + dy² + dz²)) / 256)      (dx, dy, dz = E − L)
```

The distance is three-dimensional (height counts) and floored to whole world
units. The square root runs in floating point and is rounded to the nearest
integer before the shift; the 32-bit float intermediate only matters beyond
about 65536 world units.

The emitter is the emitting object's position, with two exceptions:

- An object whose `attr_slave` attribute (index 26 in `SOUP386.DEF`) is set
  emits from its attached position: its master's position plus its own offset
  rotated by the master.
- Otherwise, an object that links to another positioned record emits from
  that record's position.

Animation PlaySound always uses the object's own position.

## Volume curve

With a signed per-call volume offset `off`:

```
if d > R:  v = 0
else:      a = clamp((d × D) >> 16, 0, 127)
           v = clamp(127 − a + off, 0, 127)
```

Volume falls linearly from 127 at the listener. With the shipped constants the
attenuation reaches 127 at `d = 2287`, so a sound with `off = 0` is already
silent there, slightly before the radius. A positive offset cannot make a
sound beyond `R` audible.

The addition of `off` wraps as a signed 32-bit sum before the final signed
clamp. An overflowing positive offset can therefore produce zero volume.

## Pan

Pan uses only the horizontal plane (x, z):

1. `a` = the engine's integer heading from listener to emitter: 0 when the
   emitter lies along +Z, 512 along +X, 1024 along −Z, 1536 along −X.
   - Take `|dx|` and `|dz|`; while either exceeds 511, halve both
     (arithmetic shift).
   - If `|dx|` is now 0: `a` = 0 when `dz ≥ 0`, else 1024. Otherwise, if
     `|dz|` is 0: `a` = 512 when `dx ≥ 0`, else 1536.
   - Otherwise, with `s = min(|dx|, |dz|)` and `b = max(|dx|, |dz|)`:
     `t = T[floor(s × 512 / b)]`. The base angle is `t` when `|dx| < |dz|`,
     `512 − t` when `|dx| > |dz|`, and 256 when equal.
   - Quadrant: only `dx < 0` gives `−base`; only `dz < 0` gives
     `1024 − base`; both negative give `base + 1024`. The result is taken
     modulo 2048.
   - `T[i] = trunc(atan(i / 512) × 65536 / π) >> 6` for `i` = 0..512, built at
     startup (truncation toward zero, then the shift).
2. `u = (a − yaw) mod 2048`.
3. Triangle wave:

| `u` | Offset `p` |
|---|---|
| 0–511 | `u >> 3` |
| 512–1023 | `((1024 − u) & 511) >> 3` |
| 1024–1535 | `−((u − 1024) >> 3)` |
| 1536–2047 | `−(((2048 − u) & 511) >> 3)` |

`pan = 64 + p`, range 1–127. Sounds straight ahead or straight behind are
centred; a quarter turn puts them fully to one side. At exactly `u = 512` and
`u = 1536` the `& 511` yields zero, so a sound exactly abeam is centred for
that single angle value. Pan does not depend on distance.

## Starting a sound

Every effect start takes an effect index, a playback-rate offset in Hz, a
volume and a pan.

- **Playback rate:** `clamp(effect sample_rate + rate_offset, 512, 44000)` Hz.
  The addition wraps at 32 bits before the signed clamp.
- **Looping:** see [SFX loop behaviour]../formats/SFX.md#loop-behavior.
- **Effect range:** a negative effect index or an index at or beyond the number
  of effects in `MAIN.SFX` (118 shipped) starts nothing.
- **Channels:** 16 sample channels. A start takes the first idle channel at or
  above a start index. When none is idle the sound is dropped; playing sounds
  are never stolen. The start index is 0 for the player's own animation sounds
  and for speech, 4 for combat hit sounds, and 6 otherwise (0 at boot until the
  first sound that resets it).
- **Ownership:** each channel records its owner object and the requested
  effect index (before any SURFACE.INI remap).
- **Handles:** a remembered sample handle identifies a reusable channel slot,
  not a particular start. Stopping by handle acts on the slot's current sample
  without comparing its owner or requested effect.
- **Master volume:** every volume set, at start and on later updates, sends
  `(v × master) >> 8` to Miles, where `master` is `SYSTEM.INI [system]
  volume` (shipped 255, engine default 256 before the file is read) or the
  menu Sound slider (see [menu]menu.md). There is no other global effect
  gain. A changed master reaches a playing sound only at its next volume set;
  one-shots keep their start volume.

### Animation PlaySound

Animation opcode 4 ([attachment](attachment.md)) starts a positional sound:

- `effect` = first signed 10-bit field, remapped through
  [SURFACE.INI]../config/surface-ini.md#sound-remap-records by the animated
  object's surface.
- `rate_offset` = second signed 10-bit field × 64 Hz.
- Volume from the curve with `off = 0`, pan from the animated object.
- The sound starts even when `v = 0`; it then occupies a channel silently.
- The handle is stored on the animation; EndAnimation (opcode 1) stops it.
  No later volume or pan update was found for these sounds.

Volume and pan use the actor and camera positions when the PlaySound command
executes. They do not use the actor's later position at the end of the update.
During ordinary player walking, animation advancement can start a sound before
forward movement and ground response change the player's position.

Each encountered PlaySound advances to the next command without consuming a
frame skip. A second PlaySound replaces the animation's remembered handle
without stopping the previous channel. Selecting a replacement group clears
that handle without issuing a stop. EndAnimation stops only the handle still
remembered by that animation. Channel validity and completion are handled by
the audio mixer independently.

The metadata field historically named `volume_shift` contains the second
signed ten-bit operand. It is a rate offset in units of 64 Hz, not a volume
adjustment.

### SOUP functions

`SOUP386.DEF` declares these sound functions:

| Function | Kind | Parameters | Behaviour |
|---|---|---|---|
| `Sound` | function | effect, volume offset, rate offset | Positional start: volume from the curve with `off` = volume offset, pan at start. Starts even at `v = 0`. No later updates. |
| `FlatSound` | function | effect, volume, rate offset | Non-positional: pan 64, `v = clamp(base_volume + volume, 0, 127)`, where `base_volume` is SFX header byte `0x0C` (64 in all 118 shipped effects). `FlatSound(e, 0, 0)` plays at 64. |
| `AmbientSound` | multitask | effect, volume offset, rate offset | Each run: compute volume and pan. If `v = 0`, or the runtime object-update exclusion is set, stop this task's sound and end the multitask. Otherwise start the sound if it is not playing (never started, or a one-shot finished), then set the current volume and pan. |
| `AmbientRTX` | multitask | RTX label, volume offset, unused | First run: start the label's [RTX]../formats/RTX.md clip at pan 64 and its header volume, start index 0. Each run: compute volume and pan; if `v = 0` or the object has been killed, stop the clip and end the multitask; otherwise set the volume and pan. The multitask ends once the clip has finished; it is not restarted. No dialogue line or subtitle is created. |
| `EndSound` | function | effect | Stop every playing channel whose requested effect is `effect`, whatever its owner. |
| `EndMySound` | function | effect | Stop every playing channel whose requested effect is `effect` and whose owner is this object. |
| `EndMySounds` | function | — | Stop every playing channel owned by this object. |
| `StopAllSounds` | function | — | Stop all 16 channels. |

`FlatSound` adds its volume offset to the header base volume with 32-bit
wrapping arithmetic before clamping the signed result to 0–127.

`FlatSound`, `EndSound`, `EndMySound` and `EndMySounds` return zero,
including when they start or stop no channel.

`Sound` returns zero as its SOUP scalar result, including when no sample starts.

Each `AmbientSound` task retains its own sample handle in native task state.
Whenever its elapsed input is zero, or the shared sample-reset flag is set,
it discards that remembered handle before querying playback status. Discarding
the handle does not itself stop a channel. The engine's periodic sound-reset
path stops all sample channels and sets this flag, allowing active ambient
tasks to acquire replacement channels when they next run.
The `RTX` dialogue task instead completes immediately when this reset flag is set,
before checking its distance or loading an entry.

Sound maintenance runs after clock sampling and before ordinary object updates.
Dialogue-record updates run after ordinary and code-object updates. Periodic RTX
cache updates run after the dialogue-record stage.
It clears the shared reset flag each invocation. Its accumulator starts at zero
when the world loop starts and advances by the shared whole nominal ticks when
the runtime inhibition flag is clear. A signed accumulator value greater than
1400 triggers the reset and clears the accumulator; exactly 1400 does not.


RTX initialization creates twenty cache slots with no text or audio payload,
cleared occupancy, and no remembered sample. Dialogue initialization allocates
`max(dialog_max_dialog_lines, 16)` records from `SYSTEM.INI [dialog]` and clears
their stored state. The topic-menu capacity is `max(dialog_max_menu_items, 300)`,
and text width is `max(dialog_max_text_width, 150)`. A configured
`menu_traverse_delay` of zero becomes one BIOS tick. `WaitOnDialog` scans every record and remains active while
any record has its active word equal to one. It does not filter by the caller.

The separate dialogue-state word starts at zero in a freshly loaded executable.
`DialogBegin` writes one to that word; `DialogEnd` writes zero. These writes do
not replace the per-record active-state scan performed by `WaitOnDialog`, and
the dialogue-state word is separate from the topic-menu-state word.

Dialogue creation selects the first inactive record starting at index one. If
no record is available, it returns no record. An admitted record stores the
speaker, formatted text, active state and the RTX entry's remembered sample
handle. Clearing a record removes its active state and text and replaces the
remembered sample handle with the absent-handle value. It leaves the remaining
record fields intact. Querying an absent record reports inactive.

`RTXp` uses record zero and assigns the selected player as its speaker,
regardless of the calling actor. It replaces that record directly rather than
searching for an inactive record. Clearing dialogue by an RTX cache entry's
remembered sample scans active records starting at one and leaves record zero
alone. Matching uses the remembered sample slot, not speaker identity.

The `RTXp` task completes when the shared sample-reset flag is set. Otherwise,
its first zero-elapsed invocation loads the label once and replaces record zero
when lookup succeeds. Later invocations query that record's activity without
reloading the label. The handler has no speaker-distance gate.

Dialogue lifetime is initialized from the loaded audio duration when a sample
handle is remembered. Duration uses integer units of 1/256 second:
`(PCM_byte_count << 8) / (channels * sample_rate * bytes_per_sample)`.
When no sample is remembered, lifetime is fifteen times the original text byte
count before display formatting.

RTX lookup first reuses a cached entry with the same label, marking it used and
resetting its age to eight nominal ticks. Otherwise it selects the first entry
whose used flag is clear, even if that entry still retains a different label and
payload. Loading a new entry replaces its retained text and audio payloads and
marks it used with age eight. If every entry is marked used, lookup reports a
cache-exhaustion error rather than evicting a used entry. File lookup lowercases ASCII uppercase letters in the four-byte label.
A voiced entry with no remembered sample starts playback at sample start index
zero, then restores the shared start index to six.

Periodic cache updates retain an entry unchanged while its remembered sample has
playing status four. Otherwise they clear the remembered sample. An entry used
since the preceding cache update clears its used flag and resets its age to eight
nominal ticks. An unused entry is released when its age is strictly less than the
current nominal tick count; equality subtracts to zero and retains the payload.
Release clears text, audio, occupancy, label and remembered sample.
The full cache reset releases text first, stops a remembered sample only when its
status is four, then releases audio and clears occupancy, sample and label.

Dialogue record updates visit records in index order. Each active record first
sets its speaker's dialogue-inhibition attribute to one, then subtracts 21 times
the whole nominal tick count from its signed lifetime counter. With speech
enabled, a remembered sample that is still playing has its volume refreshed
and its line presented; a finished sample clears the line. Either sample case
ends the update. A line without a remembered sample remains active and traversal
continues. With speech disabled, a positive remaining lifetime presents the line
and ends the update; an expired line is cleared and traversal continues.

Before record traversal, dialogue updates edge-detect their skip-input signal.
The signal is the combined `KEYS.INI` `key_a` binding pair, the walk/run modifier.
A released signal clears the latch. A new pressed signal sets the latch and,
only when player lockout equals one, visits every record, stops any remembered
sample that is playing, clears the record, and ends the update. Holding the
signal does not repeat that cleanup. The latch starts clear. Fresh dialogue
records have zero-valued sample fields; clearing replaces these with the absent
handle, so fresh inactive records and cleared records have different stored
sample values.

Combat taunts retain an RTX cache reference per actor. Replacing that reference
releases the old entry when it remembers a sample; an absent remembered sample
leaves the cached payload intact. In both cases the actor drops its reference.
The new label uses the shared RTX cache. After lookup, its remembered sample is
stopped and cleared before the new taunt is presented, even when the label was
already cached. This lookup differs from ordinary dialogue reuse, which retains
a remembered sample.

For a nonzero taunt label, presentation releases the retained cache reference
first, then clears its retained dialogue record if that record is active and
`dialog_print_text` is enabled. It loads the requested label and stops its
remembered sample. A successful lookup starts speech only when
`dialog_use_speech` equals one and creates a dialogue record only when
`dialog_print_text` is nonzero. A nonzero accompanying animation is requested
with modifier zero after lookup. A zero label skips replacement and animation.
The result is zero when text is disabled; otherwise it is the retained dialogue
record's signed active word, or zero for the absent record. Combat construction
initializes the retained record index and result to zero and the cache reference
to absent.

The reset releases cached RTX text and audio payloads and clears their occupied
state. It also clears active dialogue records and their text buffers, resets
their sample handles. These operations precede stopping all sample channels
and setting the shared reset flag. It then clears the ownership byte of every
node in every loaded walk-node map, regardless of the owning actor.

The runtime menu-state word has an initial value of zero.
`MenuBegin` sets it to one; `MenuEnd` clears it.
The SOUP `menuProc` task activates this state when it is inactive and clears
it when a selection is accepted. These calls occur inside the native task
handler.
Engine initialization clears maintenance inhibition. World-loop entry resets
only the accumulator. During player-controller execution, an active menu-state
word or a nonzero
`WaitOnDialog` result sets the maintenance inhibition flag. When both are zero,
the flag clears only if player lockout is also zero. A nonzero player lockout
therefore retains the prior inhibition value. This state is sampled by the
player controller, rather than every ordinary object update. The menu-state word is separate
from the dialogue-state word controlled by `DialogBegin` and `DialogEnd`.

[`TaskStop`](SOUP.md#animation-task-ownership-and-stopping) removes the native
task slot without invoking its handler. Stopping an ambient task therefore
does not itself issue a sample-stop command. Sound ownership commands are separate from task-slot removal.

`KillMe` sets the object's killed flag and stops every channel the object
owns; `Resurrect` clears the flag.
`KillMe` releases held walk-node reservations, clears the current model
reference, sets the object's killed flag to 1 and runtime death-state selector
to 8, and stops every channel the object owns.

`Resurrect` always releases held walk-node reservations. When the killed flag
is 1 or the runtime death-state selector is 8, it clears the killed flag. If
the actor has combat state, it reinitializes that state and consumes one waiting
SOUP message when present.

**Owner:** the owner recorded by `Sound`, `FlatSound`, `AmbientSound` and
`AmbientRTX`, and matched by `EndMySound` and `EndMySounds`, is the calling
object, except for two kinds of script that run on a temporary object:

- Scripts run by the shell system (for example an item's `use_script`, see
  [ITEM.INI]../config/item-ini.md) record the shell entry as owner.
- Scripts of code objects (scripts without a world object) record the code
  object as owner.

So an ambient sound follows the listener while its multitask runs, restarts
non-looping effects when they end, and stops for good once the listener leaves
its range; the script must call it again to resume.

### Engine-internal starts

- **Combat hits:** effect from the attacker's `hit_sound` attribute,
  `off = 0`, `rate_offset = random(0..127) × 16 − 1024` Hz (−1024 to
  +1008), start index 4. Not started when `v = 0`.
- **Speech:** [RTX]../formats/RTX.md clips start at pan 64 with rate offset
  0 and volume `clamp(header byte 0x0C, 0, 127)` (100 in the shipped
  `ENGLISH.RTX` audio headers), start index 0. While the dialogue runs, the
  volume is refreshed every frame from the speaker's distance (curve with
  `off = 0`); the pan is not updated. The raw RTX sample start clears its
  sample user-data slots and does not assign an object owner. Dialogue speaker
  identity is retained separately by the dialogue record.
- **Rope sounds:** each [MPRP]../formats/RGM.md#mprp-rope-chains rope
  record names three effects. A rope plays one of them when its swing
  velocity changes sign (or stays at zero), chosen by its current swing speed
  against `q = trunc(m / 4)`, where `m` is the rope's maximum swing speed:
  the third effect from `3q`, the second from
  `2q`, the first from `q`; a zero effect falls through to the next lower one.
  Ropes whose `q` is 2 or less play nothing. Volume from the curve with
  `off = 0` at the rope's position, rate offset 0, start index 6; not started
  when `v = 0`. Of the 29 shipped rope records, 23 name effects 62, 62, 63,
  one 62, 62, 62, and five none.
- **Torch:** while the player's torch burns, effect 25 plays from the player
  with `off = −40` on one shared channel, owned by the player. It is
  restarted whenever it has finished, its volume and pan are refreshed every
  frame, and it stops when the torch goes out.

## Mixer gain and pan law

The statically linked Miles mixer derives channel scaling from the sample's
volume, pan and PCM sample width whenever volume or pan changes.

Volume and pan are clamped to 0–127. The effective volume is
`v = clamp(trunc(volume × driver_master / 127), 0, 127)`. The driver master
starts at 127 and the game never changes it, so `v` equals the sample volume.

Pan uses a 128-entry table `T`: `T[p] = 2p` for `p` 0–62 and `T[p] = 128`
for 63–127. The left side uses `T[127 − pan]`; the right side uses `T[pan]`.
At pan 64 both entries are 128. Towards either extreme the far side fades
while the near side retains its centre level.

For 8-bit PCM, the mixer builds lookup tables. Set `a = v + 1` when `v` is
nonzero, otherwise `a = 0`. For a sample amplitude `s` expressed in signed
16-bit units and the corresponding pan-table value `t`, the table contribution
is `floor(floor(s × a / 128) × t / 128)`. At the centre, nonzero volume
therefore scales by `(v + 1) / 128`.

For 16-bit PCM, the mixer computes a scalar coefficient for each side:
`c = floor(t × v / 127)`. A signed sample contributes `floor(s × c / 128)`
to that channel. The same coefficient scaling applies when the mixer advances
the source at a different playback rate. There is no nonzero-volume increment
in this path. At the centre, volumes 0–126 scale by `v / 128`; volume 127
has unity gain. A mono sample supplies both output channels.

## SURFACE.INI remap

At startup the engine builds a table `[surface 0..11][sound 0..255] → effect`,
each row initialised to identity, from the [SURFACE.INI](../config/surface-ini.md)
sound sections in this order: `unknown` (0), `water` (1), `deepwater` (2),
`scapewater` (3), `scapedeepwater` (4), `lava` (5), `sand` (6), `wood` (7),
`tile` (8), `scape` (9), `rock` (10), `gloop` (11).

Only animation PlaySound uses it: when the animated object's surface value is
non-zero, `effect = table[surface][effect]`. `Sound`, `AmbientSound`, combat
hits and speech are not remapped. The configured `unknown` row is bypassed.
Missing sound sections retain identity; repeated entries overwrite earlier
ones. Targets are unsigned 16-bit effect IDs and still pass through the sound
bank's playback validation.

The lookup helper accepts nonzero surface codes up to and including 12, though
only rows 0 through 11 exist. Code 12 reads beyond the allocated table and has
no defined remap. Codes greater than 12 and an absent remap table return the
input effect unchanged. There is no source-ID bounds check in this helper;
animation callers must supply an ID in `0..255` for a valid row lookup.

An object's surface value comes from the face it stands on:

- A model face: the surface type that `[surfaces]` assigns to the face's
  texture, or 0 when the texture has no entry.
- Terrain: each query first resolves texture set 302 and the tile's low-six-bit
  image index through `[surfaces]`. The terrain blend mask can replace that
  value with sand or rock at dry shore pixels. A nonzero water result then
  selects `scapewater` (3), or `scapedeepwater` (4) for full water coverage.
  A dry result keeps the surface computed by this query, not a previous
  object's terrain-water result.
- Other ground-query source kinds assign 0 on the ordinary assignment path.

This uses the ground-support query, not an arbitrary side collision. Model
support supplies the packed texture of the selected face; solid-color faces
supply zero. Terrain support queries the actor's integer map X/Z. An
out-of-grid terrain tile lookup returns `-1`, which supplies image 63 after
the low-six-bit mask, before the usual assignment and coverage rules.

The support wrapper updates actor surface state only when collision attribute
mask `0x04` is set. Excluded and query-only paths retain the previous actor
surface. A support miss reaches the updater with a cleared source kind, so it
assigns zero only if the ordinary assignment path is reached. See
[surface-query state retention](player.md#surface-query-state-retention).

## Shipped script calls

The 27 shipped RGM files contain 137 `Sound`, 477 `FlatSound`, 91
`AmbientSound`, 97 `AmbientRTX`, 33 `EndMySounds` and 12 `StopAllSounds`
calls, and none to `EndSound` or `EndMySound`. Values outside the usual
ranges occur: `FlatSound` volume 128 (clamped to 127) and rate offset −10000
(clamped to 512 Hz), `AmbientSound` rate offset 2000, `AmbientSound` effect 127
(beyond the 118 shipped effects, so it never sounds), and `AmbientRTX` volume
offsets of 127 and 128 (volume 127 everywhere within the radius `R`). Every
`AmbientRTX` call passes 0 as its third argument.

## Related

- [SFX]../formats/SFX.md — effect headers (sample rate, base volume, loop flag)
- [RTX]../formats/RTX.md — speech clips
- [SURFACE.INI]../config/surface-ini.md — sound remap sections
- [SYSTEM.INI]../config/system-ini.md — `volume`, `sound_distance`
- [Item Attachment System]attachment.md — animation opcode 4
- [Book menu]menu.md — Sound slider
- [Music]music.md — CD audio, separate from effects