Repository navigation
Distance cues: level reverb sends and facing voices - #166
Merged
Merged
Conversation
Cacophony takes a send from a voice's output, after its gain (routableSource.ts: outputNode.connect(sendGain), where outputNode is the gain node that follows the panner). The client applies a positioned sound's distance attenuation at that same gain, so the level reaching the room's reverb fell exactly as fast as the direct level. A far sound was quieter but no wetter than a near one, and the direct-to-reverb ratio, the main cue for distance other than loudness, never changed. The send is now given the gain send / distanceGain, so the chain receives volume x send wherever the source is and the direct level alone falls with distance. It is re-gained in place whenever the distance gain changes (the listener or the source moves, the spatial profile changes) and whenever the send or the chain changes. The feed from a sound's inline effect chain into its named chain is downstream of the same gain and is compensated the same way. The boost is capped at 32 x (+30 dB). The engine accepts any finite send gain and does not clamp it, and the distance curves give no usable limit: the default one floors at 1 / 10 000 and a linear one reaches 0, where the tap is silent. Under the default curve the reverberant level is even out to 32 m and falls with the direct sound beyond. A change of send on a live aux send now re-gains it in the engine instead of removing the send and adding it again. The direct level, the distance model and occlusion are unchanged: occlusion sits before the tap and still dims direct and reverb alike. Non-positional sounds have no distance gain and keep their wire send. Ambisonic sounds do not use named chains.
A voice in the scene was an omnidirectional point source: the speaking entity's forward vector arrives with Client.Spatial.EntityMove and is tweened into the spatial store, and nothing read it. A positional voice now takes its orientation from that vector, already in Web Audio axes, with a gentle cone: full level within 60 degrees either side of the speaker's facing, falling evenly to -6 dB directly behind (panner cone 120 / 360 / 0.501). The facing follows the entity through the store subscription that already moves the voice; a turn ramps like a move. An entity with no forward has no cone, and a non-positional voice has no panner at all, as before. The distance curve was already the one media sounds use by default: the voice panner took the same three constants DEFAULT_SPATIAL_PROFILE is built from. It is now derived from that profile by name (profilePannerDistance), so the two cannot drift apart. Web Audio's inverse model is the profile's formula exactly, so the rolloff stays in the panner and there is no separate distance gain for a voice. That rolloff is inside the panner, which is ahead of the gain node the engine taps a send from, so a voice's reverb send fell with distance just as a sound's did. The send is now given send / distanceGain with the same cap, where the distance gain is computed from the profile, the voice's position and the engine's listener position, since a panner's gain cannot be read back. It is re-gained in place when the speaker or the listener moves and when the send or chain changes. The cone is also ahead of the tap: a speaker facing away sends up to 6 dB less to the room. That is not compensated here.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two distance cues for positioned audio. One commit each, tests with each.
1. The reverb send no longer falls with distance
Cacophony takes a send from a voice's output, after its gain (
routableSource.ts:outputNode.connect(sendGain), andoutputNodeis the gain node that follows the panner). The client applies a positioned sound's distance attenuation at that same gain, so the level reaching the room's reverb fell exactly as fast as the direct level: a far sound was quieter but no wetter than a near one.The send is now given
send × min(1 / distanceGain, 32), so the chain receivesvolume × sendwherever the source is, and only the direct level falls with distance.sendor the chain changes. All of those already pass throughapplyLevels.sendon a live aux send now re-gains it in the engine. Before, the send was removed and added again.The cap.
MAX_SEND_DISTANCE_BOOST = 32(+30 dB), indistanceModel.ts. The engine accepts any finite send gain and does not clamp it, and the distance curves give no usable limit: the default one floors at 1 / 10 000 and a linear one reaches 0, where the tap is silent and nothing can be recovered. So the cap is a choice: under the default curve the reverberant level is even out to 32 m, and beyond that it falls with the direct sound. It caps the boost, not the send, so that distance is the same whatever the send.2. Voices have a facing, and their reverb send is level with distance too
forward, whichClient.Spatial.EntityMovealready delivers and tweens into the spatial store (in Web Audio axes), with a gentle cone: full level within 60° either side of the facing, falling evenly to −6 dB directly behind (VOICE_CONE_INNER_ANGLE120,VOICE_CONE_OUTER_ANGLE360,VOICE_CONE_OUTER_GAIN10^(−6/20)).forwardis omnidirectional. A non-positional voice has no panner, so no cone and no distance, as before.DEFAULT_SPATIAL_PROFILEis built from. It is now derived from that profile by name (profilePannerDistance). Web Audio's inverse model is the profile's formula exactly, so the rolloff stays in the panner; there is no second gain stage.send / distanceGainwith the same cap. A panner's gain cannot be read back, so the distance gain is computed from the profile, the voice's position and the engine's listener position.Not done
Existing tests changed
Media.test.ts, "rebuilds on an Update to 3D, carrying the state the voice has now": the rebuilt 3D voice is 2 m away, so its send is now 0.6, not the wire 0.3. The non-positional case still asserts 0.3, and both assert the carried wire send.LiveKitSpatialAudioBridge.test.ts: the twoattachVoiceargument assertions gain the third argument (null, no facing), and the mock voice gainssetFacing.Verification
npm run typecheck,npm test(128 files, 1564 tests) andnpm run test:audio-cachepass locally. Nothing was listened to: this is verified by unit tests against mocked engine objects and by reading the engine source, not by ear and not in a browser.