Sound
How much sound to build
1. Keep continual ambient sound light
The SL-style viewer's audio engine renders spatial loops crudely: no occlusion, loops that sum across a region until each has a radius, a "digital blurble" from grainy layers at distance, and audible loop seams. An evening of mix tuning bought modest returns; the visual and interactive world repaid the effort far better. The advice recorded in this project is to keep a few beds (water, fire, wind, waves) and otherwise add sound as short EVENT one-shots tied to an interaction (a latch, a creak), not new continual ambience. The later build logs did add more loops (wind, rain, a corona hiss); CONFLICTS.md notes the tension. The techniques below are what made the loops that exist work.
Making and uploading
2. Sounds are OGG Vorbis, played by UUID
Encode with ffmpeg -c:a libvorbis, upload with WorldBridge upload_sound (the file must start OggS), and play by
the returned UUID (llLoopSound, llPlaySound, llTriggerSound). Keep each clip under ten seconds.
- See:
tools/wire_sounds.py,tools/make_ambient_sounds.py.
3. A looped noise buffer must be seamless
Loop seams are audible. The ambient beds were built as seamless loops (the wind from soft band-limited noise in layers). A pitched carrier under bubbles read as a "digital blurble"; band-limited noise replaced it.
- See:
tools/make_water_sounds.py,tools/make_wind_sounds.py.
4. Fade a clip where its sound ends, not where its buffer ends
A clip compressed to carry its tail must be faded where the sound actually ends, and trimmed; the lifted tail of a thunder roll otherwise stopped dead.
5. Verify a sound offline by its spectrogram and envelope, not by playing it aloud
Check spectrogram and envelope sheets of the WAV. A speech-to-text transcriber run on a WAV file is a device-free first ear: its labels are hints (a gull cry pitched near 600 Hz with a vocal buzz was transcribed as "(crying)", which is a baby's range).
Placing sound in the world
6. Sounds carry far and sum: give every voice a radius
With no limit, nine brook emitters were heard across the whole island. Give every ambient voice
llSetSoundRadius(r), a hard audible edge (about 10-16 m for water).
- Prove "where is that sound coming from" by A/B: silence the suspects, listen, restore.
7. Mind the room
A loud source 2-5 m outside a wall is loud indoors too; there is no occlusion. Put outdoor voices away from the house and raise the indoor one.
- Learned: by the fire, the mill drowned the fire's crackle until the mill's voices moved out and down and the hearth went to 0.9.
8. Put emitters at the listener's head
A triggered sound from 6 m overhead is quietened by the viewer's distance rolloff. Weather sounds ride an emitter that follows the nearest visitor. A sound triggered at the harbour faded out in the viewer by about 60 m, so a "far voice" at the visitor's head plays the distant version (low-passed, with echoes) instead.
- See:
tools/build_thunder.py,tools/build_wind.py,tools/build_shipbell.py.
9. Balance by crest factor, not by peak
A noise loop normalised to full scale is far louder ON AVERAGE than a one-shot like thunder (mostly a quiet roll under one crack). Wind loops at 1.0 buried the thunder; 0.12-0.45 let it stand about 20 dB out.
10. llPlaySound stops the previous sound; llTriggerSound overlaps
For a bell whose strokes should ring on under the next, use llTriggerSound.
11. Make a loop one breath long to breathe with a light
To make a sound swell with a glow, make the loop exactly one breath period long and start it on the shared clock's rising edge.
One setting for every sound
12. Every sound script obeys one mode
A small object (a bell) holds the region's sound mode: on, proximity (a sound plays only while someone is near it,
and carries less far) or off. It remembers the mode in its description, announces it region-wide, re-announces it
every five minutes, and answers scripts that start later and ask. Every sound script carries the shared snippet:
sound_mode.LOOP_LSL for loops (loop_setup(sound, volume, radius) plus LOOP_EVENTS) or EVENT_LSL for
one-shots (guard each play with may_sound(radius)). A new sound script without it is a bug: nobody can silence it.
- See:
tools/sound_mode.py(python tools/sound_mode.py <mode>sets the mode from outside throughregion_say).
13. Clear the restored loop before applying a mode
A prim restores its last looping sound from the region database at startup, before its script runs. loop_setup
calls llStopSound() first, or a silenced region plays again after a restart.
What is really playing
14. The region database is not the live truth
The database's LoopedSound column goes stale: a prim whose only change is its sound is never marked changed. Read
the live sound with WorldBridge inspect (sound: asset, gain, flags where 1 = looping, radius).
- See:
tools/sound_check.py(--modeswalks the mode through on, off, proximity, on and counts the looping voices each time).
15. Listen to what the viewer actually plays
Record the host's audio output (loopback capture) while the viewer is in the world. That is the true in-world listen, and it catches what offline checks cannot.
- Learned: every offline check passed, and the viewer's output still showed the wind heard as "an engine revving" (a low band swelling with each gust), the thunder buried under the wind and quietened by rolloff, the roll stopping dead, and the crack clipping the mix.
- Analysis: the capture is float WAV-extensible; convert it with ffmpeg to f32 first. Band-split it into half-second RMS (20-250 Hz, 250-1500 Hz, 1.5-8 kHz) to see an event rise out of the bed. For a rhythmic event, a broadband onset detector is fooled by surf, flame and birds; a MATCHED FILTER (cross-correlate with the sound's own first 0.3-0.4 s), band-limited to the event's upper partials (1.2-2.8 kHz for a ship's bell), found every stroke and its timing to 0.02 s.
- Numbers matter: 26 drops a second is a few drips; rain is hundreds.