flutter3d_audio
Positional audio: attenuation, panning, occlusion and voice limiting, with a
pluggable backend. It is a sibling of flutter3d_game, not a RenderPlugin:
it draws nothing, so the renderer's plugin seam does not apply to it.
It depends on neither flutter3d nor flutter_gpu. It is handed a listener
pose and emitter positions, and where those come from is up to the application.
Why the geometry is computed here
SoLoud has a 3D layer, but it cannot be used for anything that moves. Applying a
moved source or a turned listener requires update3dAudio(), and
flutter_soloud 4.1.7 exposes neither that call nor anything that makes it. Its
set3dSourcePosition passes straight through to a SoLoud method that only marks
the source dirty. A source set once at play3d never moves again, and in a
first-person game the listener turns constantly.
So AudioScene computes gain and pan, and the backend is asked only for a flat
voice with a volume and a pan. Every backend can do that, and it takes effect
immediately. If a later flutter_soloud exposes the call, the right change is a
second backend, not an edit to this one.
Shape
// The game's own sounds.
abstract final class Sounds {
static const pistol = SoundDef(name: 'pistol', asset: 'audio/pistol.ogg');
static const door = SoundDef(name: 'door', asset: 'audio/door.ogg');
static const torch = SoundDef(
name: 'torch',
asset: 'audio/torch.ogg',
loop: true,
);
}
final scene = AudioScene(
backend: SoLoudBackend()..open(),
maxVoices: 24,
// The walls belong to the physics; this package must not learn about them.
occlusion: (from, to) => world.raycast(from, to) ? 0.3 : 1.0,
);
await scene.preload(<SoundDef>[Sounds.pistol, Sounds.door]);
scene.play(Sounds.pistol, muzzlePosition); // one-shot
final hum = scene.play(Sounds.torch, torchPosition); // holds a looping emitter
// Once a frame, after everything has moved.
listener.aimAt(eyePosition, yaw);
scene.update(listener);
play returns an emitter so whatever owns a moving source can move it. The
mixer updates that voice instead of restarting it.
Opening the speakers, and where the ears are
openSpeakers opens the device and hands back Speakers: the backend, to
pause, resume and dispose, and the scene, to play through. When there is no
device it throws AudioDeviceException, and a game catches it and plays
silent. What the backend could not play it reports as an Issue, the same
type flutter3d_app reports in.
Positions are metres from an origin the game names, as float32 offsets: the
scene's origin, which a scene graph keeps near the camera. A place in the
world, a WorldPosition in doubles, comes in through placeAt with that
origin. Flutter3dView says where the camera is after every frame, so the
listener is one line:
Flutter3dView(
onListenerMoved: (ears) => listener.placeAt(
ears.position,
ears.forward,
origin: ears.origin,
up: ears.up,
),
onPausedChanged: (paused) =>
paused ? speakers.pause() : speakers.resume(),
)
What the mixer decides
- Attenuation is
InverseRolloffby default, the only one of the three that sounds right when the player walks past something.LinearRolloffandExponentialRolloffexist because designers ask for them, andNoAttenuationis for music. - Panning uses the left-right component of the direction and nothing else. It is yaw only, so tilting your head back does not swap left and right.
- Voice limiting ranks by priority first and loudness second. A door closing outranks the ninth footstep even when the footstep is nearer.
- With instance limiting, ten identical grunts on one frame are a click instead of ten times as loud.
Testing
SilentBackend records every call and makes no sound, so the whole mix is
testable without an audio device. A headless build, or a player who has turned
sound off, should use it too, instead of a branch at every call site.
Part of flutter3d, an independent
implementation of a 3D engine for Flutter. It is not a fork or a binding of
another engine, and it is not affiliated with Google or the Flutter project. It has four
switchable rendering backends: Impeller via Flutter GPU, WebGL2, WebGPU and a
software rasteriser. It loads glTF, OBJ and .f3d, and has six lighting models,
shadows, bloom, skinning, animation, BVH culling and picking, plus a
deterministic fixed-step game layer with collision, navigation, positional
audio, and gamepad and touch input. Four example games (shooter, platformer,
racing, strategy) are each built on a genre package:
flutter3d_game_shooter,
flutter3d_game_platformer,
flutter3d_game_racing,
flutter3d_game_strategy.
A new game starts from the editor's scaffold, which writes one from a template:
flutter3d.pleion.dev/first-project/. Documentation:
flutter3d.pleion.dev.
Libraries
- flutter3d_audio
- Positional audio for flutter3d.