flutter3d
Showcase Changelog 38 packages API reference

Voice limiting: the source

since 0.1.0 Simulation, audio and XR

Regions the guide quotes: live (line 78), scene (line 178), crowd (line 183), read (line 190).

1/// How many sounds may play at once, and which ones win when more than that
2/// are asking to be heard.
3///
4/// Quoted by `voice_limit.md` and shown whole in the Source tab.
5library;
6
7import 'dart:math' as math;
8
9import 'package:flutter3d/flutter3d.dart';
10import 'package:flutter3d_audio/flutter3d_audio.dart';
11import 'package:flutter3d_showcase/src/demo/demo.dart';
12import 'package:flutter3d_showcase/src/demo/scene_kit.dart';
13import 'package:vector_math/vector_math.dart';
15final class VoiceLimitDemo extends ShowcaseDemo {
16 late final String _report;
18 double voices = 3.0;
19 double asking = 8.0;
20 bool shouting = true;
22 bool _dirty = true;
23 late final List<MeshNode> _balls;
24 late final MeshNode _shout;
25 late final BarGauge _granted;
27 static const int _most = 12;
29 @override
30 void configureView(DemoContext context) {
31 context.orbit
32 ..distance = 14.0
33 ..pitch = 1.1
34 ..yaw = 0.0;
35 context.orbit.target.setValues(0.0, 0.0, 0.5);
36 }
38 @override
39 Scene build(DemoContext context) {
40 _report = _run();
41 _balls = <MeshNode>[
42 for (var i = 0; i < _most; i++)
43 ballNode(context, 'step $i', 0.3, Vector4(0.3, 0.3, 0.33, 1.0)),
44 ];
45 _shout = ballNode(
46 context,
47 'shout',
48 0.55,
49 Vector4(0.3, 0.3, 0.33, 1.0),
50 at: Vector3(0.0, 0.55, -3.0),
51 );
52 _granted = BarGauge(
53 context,
54 'granted',
55 Vector4(0.7, 0.4, 0.8, 1.0),
56 Vector3(-5.0, 0.0, 5.5),
57 height: 10.0,
58 );
59 return sceneOf(<SceneNode>[
60 floorNode(context, width: 16.0, depth: 14.0),
61 blockNode(
62 context,
63 'listener',
64 Vector3(0.6, 0.6, 0.6),
65 Vector4(0.45, 0.65, 0.95, 1.0),
66 at: Vector3(0.0, 0.3, 0.0),
67 ),
68 ..._balls,
69 _shout,
70 ..._granted.nodes,
71 ]);
72 }
74 @override
75 void update(DemoContext context, double dt) {
76 if (!_dirty) return;
77 _dirty = false;
78 // Ask again with whatever the sliders say: a scene with room for `voices`
79 // voices, a crowd of footsteps at growing distances, and a shout.
80 final AudioScene scene = AudioScene(
81 backend: SilentBackend(),
82 maxVoices: voices.round(),
83 );
84 const SoundDef footstep = SoundDef(
85 name: 'footstep',
86 asset: 'step.wav',
87 priority: 0,
88 );
89 const SoundDef shout = SoundDef(
90 name: 'shout',
91 asset: 'shout.wav',
92 priority: 10,
93 );
94 final List<SoundEmitter> steps = <SoundEmitter>[
95 for (var i = 0; i < asking.round(); i++)
96 scene.play(
97 footstep,
98 Vector3(
99 (1.5 + 0.35 * i) * math.cos(i * 2.4),
100 0.0,
101 (1.5 + 0.35 * i) * math.sin(i * 2.4),
102 ),
103 ),
104 ];
105 final SoundEmitter? loud = shouting
106 ? scene.play(shout, Vector3(0.0, 0.0, -3.0))
107 : null;
108 scene.update(AudioListener());
109 for (var i = 0; i < _most; i++) {
110 final bool asked = i < steps.length;
111 _balls[i].visible = asked;
112 if (!asked) continue;
113 final double angle = i * 2.4;
114 final double radius = 1.5 + 0.35 * i;
115 _balls[i].setPosition(
116 radius * math.cos(angle),
117 0.3,
118 radius * math.sin(angle),
119 );
120 _colour(_balls[i], steps[i].audibleGain > 0.0, const <double>[
121 0.9,
122 0.75,
123 0.3,
124 ]);
125 }
126 _shout.visible = loud != null;
127 if (loud != null) {
128 _colour(_shout, loud.audibleGain > 0.0, const <double>[0.85, 0.35, 0.3]);
129 }
130 _granted.set(scene.voiceCount / 10.0);
131 }
133 /// Lit in [lit] colours while it has a voice, dark once it has none.
134 void _colour(MeshNode node, bool has, List<double> lit) {
135 node.material.baseColor.setValues(
136 has ? lit[0] : 0.22,
137 has ? lit[1] : 0.22,
138 has ? lit[2] : 0.25,
139 1.0,
140 );
141 }
143 @override
144 List<DemoControl> controls(DemoContext context) => <DemoControl>[
145 SliderControl(
146 'Voices allowed',
147 min: 1,
148 max: 10,
149 value: () => voices,
150 onChanged: (double v) {
151 voices = v.roundToDouble();
152 _dirty = true;
153 },
154 format: (double v) => v.round().toString(),
155 ),
156 SliderControl(
157 'Footsteps asking',
158 min: 0,
159 max: 12,
160 value: () => asking,
161 onChanged: (double v) {
162 asking = v.roundToDouble();
163 _dirty = true;
164 },
165 format: (double v) => v.round().toString(),
166 ),
167 ToggleControl(
168 'Add a shout',
169 value: () => shouting,
170 onChanged: (bool v) {
171 shouting = v;
172 _dirty = true;
173 },
174 ),
175 ];
177 static String _run() {
178 // A scene with room for three voices, asked for five.
179 final scene = AudioScene(backend: SilentBackend(), maxVoices: 3);
180 const footstep = SoundDef(name: 'footstep', asset: 'step.wav', priority: 0);
181 const shout = SoundDef(name: 'shout', asset: 'shout.wav', priority: 10);
183 for (var i = 0; i < 4; i++) {
184 scene.play(footstep, Vector3(i.toDouble(), 0.0, 0.0));
185 }
186 final loudOne = scene.play(shout, Vector3(0.0, 0.0, 3.0));
187 const requested = 5;
188 scene.update(AudioListener());
190 // A one-shot that lost the vote for a voice is dropped on the same
191 // update that decided it, which is why what is left over answers both
192 // questions at once.
193 final gotVoice = scene.voiceCount;
194 final shoutWon = loudOne.audibleGain > 0.0;
196 return '$gotVoice of $requested requested sounds got a voice; '
197 'the rest, being one-shots, are already gone\n'
198 'the higher-priority shout is one of them: $shoutWon';
199 }
201 @override
202 void verify(Scene scene, FrameResult frame) {
203 if (frame.drawCalls < 1) {
204 throw StateError('the crowd marker was not drawn');
205 }
206 if (!_report.contains('3 of 5 requested sounds got a voice')) {
207 throw StateError(
208 'a scene limited to three voices should give exactly '
209 'three of five sounds one',
210 );
211 }
212 if (!_report.contains('is one of them: true')) {
213 throw StateError(
214 'a higher-priority sound should win a voice over a '
215 'lower-priority one',
216 );
217 }
218 }