-
Notifications
You must be signed in to change notification settings - Fork 15
/
soloud.dart
1954 lines (1804 loc) · 68.3 KB
/
soloud.dart
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
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// ignore_for_file: require_trailing_commas, avoid_positional_boolean_parameters
import 'dart:async';
import 'dart:ffi' as ffi;
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_soloud/src/audio_isolate.dart';
import 'package:flutter_soloud/src/audio_source.dart';
import 'package:flutter_soloud/src/enums.dart';
import 'package:flutter_soloud/src/exceptions/exceptions.dart';
import 'package:flutter_soloud/src/filter_params.dart';
import 'package:flutter_soloud/src/soloud_capture.dart';
import 'package:flutter_soloud/src/soloud_controller.dart';
import 'package:flutter_soloud/src/sound_handle.dart';
import 'package:flutter_soloud/src/sound_hash.dart';
import 'package:flutter_soloud/src/utils/loader.dart';
import 'package:http/http.dart' as http;
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
/// The events exposed by the plugin.
enum AudioEvent {
/// Emitted when audio isolate is started.
isolateStarted,
/// Emitted when audio isolate is stopped.
isolateStopped,
/// Emitted when audio capture is started.
captureStarted,
/// Emitted when audio capture is stopped.
captureStopped,
}
/// The main class to call all the audio methods that play sounds.
///
/// This class has a singleton [instance] which represents the (also singleton)
/// instance of the SoLoud (C++) engine.
///
/// For methods that _capture_ sounds, use [SoLoudCapture].
interface class SoLoud {
/// The private constructor of [SoLoud]. This prevents developers from
/// instantiating new instances.
SoLoud._();
static final Logger _log = Logger('flutter_soloud.SoLoud');
/// The singleton instance of [SoLoud]. Only one SoLoud instance
/// can exist in C++ land, so – for consistency and to avoid confusion
/// – only one instance can exist in Dart land.
///
/// Using this static field, you can get a hold of the single instance
/// of this class from anywhere. This ability to access global state
/// from anywhere can lead to hard-to-debug bugs, though, so it is
/// preferable to encapsulate this and provide it through a facade.
/// For example:
///
/// ```dart
/// final audioController = MyAudioController(SoLoudPlayer.instance);
///
/// // Now provide the audio controller to parts of the app that need it.
/// // No other part of the codebase need import `package:flutter_soloud`.
/// ```
///
/// Alternatively, at least create a field with the single instance
/// of [SoLoud], and provide that (without the facade, but also without
/// accessing [SoLoud.instance] from different places of the app).
/// For example:
///
/// ```dart
/// class _MyWidgetState extends State<MyWidget> {
/// SoLoud? _soloud;
///
/// void _initializeSound() async {
/// // The only place in the codebase that accesses SoLoudPlayer.instance
/// // directly.
/// final soloud = SoLoudPlayer.instance;
/// await soloud.initialize();
///
/// setState(() {
/// _soloud = soloud;
/// });
/// }
///
/// // ...
/// }
/// ```
static final SoLoud instance = SoLoud._();
/// the way to talk to the audio isolate
SendPort? _mainToIsolateStream;
/// internally used to listen from isolate
StreamController<dynamic>? _returnedEvent;
/// the isolate used to spawn the audio management
Isolate? _isolate;
/// the way to receive events from audio isolate
ReceivePort? _isolateToMainStream;
/// A helper for loading files that aren't on disk.
final SoLoudLoader _loader = SoLoudLoader();
/// The backing private field for [isInitialized].
bool _isInitialized = false;
/// Whether or not is it possible to ask for wave and FFT data.
bool _isVisualizationEnabled = false;
/// The current status of the engine. This is `true` when the engine
/// has been initialized and is immediately ready.
///
/// The result will be `false` in all the following cases:
///
/// - the engine was never initialized
/// - it's being initialized right now (but not finished yet)
/// - its most recent initialization failed
/// - it's being shut down right now
/// - it has been shut down
///
/// You can `await` [initialized] instead if you want to wait for the engine
/// to become ready (in case it's being initialized right now).
///
/// Use [isInitialized] only if you want to check the current status of
/// the engine synchronously and you don't care that it might be ready soon.
bool get isInitialized => _isInitialized;
/// The completer for an initialization in progress.
///
/// This is `null` when the engine is not currently being initialized.
Completer<void>? _initializeCompleter;
/// A [Future] that returns `true` when the audio engine is initialized
/// (and ready to play sounds, for example).
///
/// You can call this at any time. For example:
///
/// ```dart
/// void onPressed() async {
/// if (await SoLoud.instance.initialized) {
/// // The audio engine is ready. We can play sounds now.
/// await SoLoud.instance.play(sound);
/// }
/// }
/// ```
///
/// The future will complete immediately (synchronously) if the engine is
/// either already initialized (`true`),
/// or it had failed to initialize (`false`),
/// or it was already shut down (`false`),
/// or it is _being_ shut down (`false`),
/// or when there wasn't ever a call to [init] at all (`false`).
///
/// If the engine is in the middle of initializing, the future will complete
/// when the initialization is done. It will be `true` if the initialization
/// was successful, and `false` if it failed. The future will never throw.
///
/// It is _not_ needed to await this future after a call to [init].
/// The [init] method already returns a future, and it is the
/// same future that this getter returns.
///
/// ```dart
/// final result = await SoLoud.instance.initialize();
/// await SoLoud.instance.initialized; // NOT NEEDED
/// ```
///
/// This getter ([initialized]) is useful when you want to check the status
/// of the engine from places in your code that _don't_ do the initialization.
/// For example, a widget down the widget tree.
///
/// If you need a version of this that is synchronous,
/// or if you don't care that the engine might be initializing right now
/// and therefore ready in a moment,
/// use [isInitialized] instead.
FutureOr<bool> get initialized {
if (_initializeCompleter == null) {
// We are _not_ during initialization. Return synchronously.
return _isInitialized;
}
// We are in the middle of initializing the engine. Wait for that to
// complete and return `true` if it was successful.
return _initializeCompleter!.future
.then((_) => true, onError: (_) => false);
}
/// Status of the engine.
///
/// Since the engine is initialized as a part of the
/// more general initialization process, this field is only an internal
/// control mechanism. Users should use [initialized] instead.
///
/// The field is useful in [disposeAllSources],
/// which is called from `shutdown`
/// (so [isInitialized] is already `false` at that point).
///
// TODO(filiph): check if still needed
bool _isEngineInitialized = false;
/// Used both in main and audio isolates
/// should be synchronized with each other
///
/// Backing of [activeSounds].
final List<AudioSource> _activeSounds = [];
/// The sounds that are _currently being played_.
Iterable<AudioSource> get activeSounds => _activeSounds;
/// Wait for the isolate to return after the event has been completed.
/// The event must be recognized by [event] and [args] sent to
/// the audio isolate.
/// ie:
/// - call [loadFile()] with completeFileName arg
/// - wait the audio isolate to call the FFI loadFile function
/// - the audio isolate will then send back the args used in the call and
/// eventually the return value of the FFI function
///
Future<dynamic> _waitForEvent(MessageEvents event, Record args) async {
final completer = Completer<dynamic>();
await _returnedEvent?.stream.firstWhere(
(element) {
final e = element as Map<String, Object?>;
// if the event with its args are what we are waiting for...
if ((e['event']! as MessageEvents) != event) return false;
if ((e['args']! as Record) != args) return false;
// return the result
completer.complete(e['return']);
return true;
},
// The event cannot be received from AudioIsolate.
// This could be caused when the player is deinited while some
// events are still queued.
orElse: () => false,
);
return completer.future;
}
/// Initializes the audio engine.
///
/// Run this before anything else, and `await` its result in a try/catch.
/// Only when this method returns without throwing exceptions will the engine
/// be ready.
///
/// If you call any other methods (such as [play]) before initialization
/// completes, those calls will be ignored and you will get
/// a [SoLoudNotInitializedException] exception.
///
/// The [timeout] parameter is the maximum time to wait for the engine
/// to initialize. If the engine doesn't initialize within this time,
/// the method will throw [SoLoudInitializationTimedOutException].
/// The default timeout is 10 seconds.
///
/// If [automaticCleanup] is `true`, the temporary directory that
/// the engine uses for storing sound files will be purged occasionally
/// (e.g. on shutdown, and on startup in case shutdown never has the chance
/// to properly finish).
/// This is especially important when the program plays a lot of
/// different files during its lifetime (e.g. a music player
/// loading tracks from the network). For applications and games
/// that play sounds from assets or from the file system, this is probably
/// unnecessary, as the amount of data will be finite.
/// The default is `false`.
///
/// (This method was formerly called `startIsolate()`.)
Future<void> init({
Duration timeout = const Duration(seconds: 10),
bool automaticCleanup = false,
}) async {
_log.finest('init() called');
// Start the audio isolate and listen for messages coming from it.
// Messages are streamed with [_returnedEvent] and processed
// by [_waitForEvent] when they come.
if (_isInitialized) {
_log.severe('initialize() called when the engine is already initialized. '
'Avoid this by checking the `initialized` Future before '
'calling `initialize()`.');
// Nothing to do, just ignore the call.
return;
}
// if `!_isInitialized` but the engine is initialized in native, therefore
// the developer may have carried out a hot reload which does not imply
// the release of the native player.
// Just deinit the engine to be re-inited later.
if (SoLoudController().soLoudFFI.isInited()) {
_log.warning('init() called when the native player is already '
'initialized. This is expected after a hot restart but not '
"otherwise. If you see this in production logs, there's probably "
'a bug in your code. You may have neglected to deinit() SoLoud '
'during the current lifetime of the app.');
deinit();
}
if (_initializeCompleter != null) {
_log.severe('initialize() called while already initializing. '
'Avoid this by checking the `initialized` Future before '
'calling `initialize()`.');
return _initializeCompleter!.future;
}
_activeSounds.clear();
final completer = Completer<void>();
_initializeCompleter = completer;
_isolateToMainStream = ReceivePort();
_returnedEvent = StreamController.broadcast();
_isolateToMainStream?.listen((data) {
if (data is SendPort) {
_mainToIsolateStream = data;
/// finally start the audio engine
_initEngine().then((error) {
assert(
!_isInitialized,
'_isInitialized should be false at this point. '
'There might be a bug in the code that tries to prevent '
'multiple concurrent initializations.');
if (_initializeCompleter == null) {
_log.warning(
'_initializeCompleter was set to null during initialization. '
'This might mean that deinit() was called while the engine '
'was still being initialized.');
_cleanUpUnsuccessfulInitialization();
assert(completer.isCompleted,
'Deinit() should have completed the future');
return;
}
assert(
_initializeCompleter == completer,
'_initializeCompleter has been reassigned '
'during initialization. This is probably a bug in '
'the flutter_soloud package. There should always be at most '
'one _initializeCompleter running at any given time.');
if (error == PlayerErrors.noError) {
_isInitialized = true;
/// get the visualization flag from the player on C side.
/// Eventually we can set this as a parameter during the
/// initialization with some other parameters like `sampleRate`
_isVisualizationEnabled = getVisualizationEnabled();
_initializeCompleter = null;
completer.complete();
} else {
_log.severe('_initEngine() failed with error: $error');
_cleanUpUnsuccessfulInitialization();
_initializeCompleter = null;
completer.completeError(SoLoudCppException.fromPlayerError(error));
}
});
} else {
_log.finest(() => 'main isolate received: $data');
if (data is StreamSoundEvent) {
_log.finer(
() => 'Main isolate received a sound event: ${data.event} '
'handle: ${data.handle} '
'sound: ${data.sound}');
/// find the sound which received the [SoundEvent] and...
final sound = _activeSounds.firstWhere(
(sound) => sound.soundHash == data.sound.soundHash,
orElse: () {
_log.info(() => 'Received an event for sound with handle: '
"${data.handle} but such sound isn't among _activeSounds.");
return AudioSource(SoundHash.invalid());
},
);
/// send the disposed event to listeners and remove the sound
if (data.event == SoundEventType.soundDisposed) {
sound.soundEventsController.add(data);
_activeSounds.removeWhere(
(element) => element.soundHash == data.sound.soundHash);
}
/// send the handle event to the listeners and remove it
if (data.event == SoundEventType.handleIsNoMoreValid) {
/// ...put in its own stream the event, then remove the handle
if (sound.soundHash.isValid) {
sound.soundEventsController.add(data);
sound.handlesInternal.removeWhere(
(handle) {
return handle == data.handle;
},
);
if (sound.handles.isEmpty) {
// All instances of the sound have finished.
sound.allInstancesFinishedController.add(null);
}
}
}
} else {
// if not a StreamSoundEvent, queue this into [_returnedEvent]
_returnedEvent?.add(data);
}
}
});
try {
_isolate =
await Isolate.spawn(audioIsolate, _isolateToMainStream!.sendPort);
} catch (e) {
_log.severe('Isolate.spawn() failed.', e);
_cleanUpUnsuccessfulInitialization();
_initializeCompleter = null;
completer.completeError(const SoLoudIsolateSpawnFailedException());
return completer.future;
}
_loader.automaticCleanup = automaticCleanup;
await _loader.initialize();
return completer.future.timeout(timeout, onTimeout: () {
_log.severe('initialize() timed out');
assert(_initializeCompleter == completer,
'_initializeCompleter has been reassigned');
_initializeCompleter = null;
_cleanUpUnsuccessfulInitialization();
throw const SoLoudInitializationTimedOutException();
});
}
/// Used to clean up after an unsuccessful or interrupted initialization.
void _cleanUpUnsuccessfulInitialization() {
_isolateToMainStream?.close();
_isolateToMainStream = null;
_mainToIsolateStream = null;
_returnedEvent?.close();
_returnedEvent = null;
_isolate?.kill();
_isolate = null;
_isEngineInitialized = false;
}
/// Stops the engine and disposes of all resources, including sounds
/// and the audio isolate in a synchronous way.
///
/// This method is meant to be called when exiting the app. For example
/// within the `dispose()` of the uppermost widget in the tree
/// or inside [AppLifecycleListener.onExitRequested].
///
/// (This method was formerly called `stopIsolate()`.)
void deinit() {
_log.finest('deinit() called');
/// check if we are in the middle of an initialization.
if (_initializeCompleter != null) {
_initializeCompleter
?.completeError(const SoLoudInitializationStoppedByDeinitException());
_initializeCompleter = null;
}
/// reset broadcast and kill isolate
_isolateToMainStream?.close();
_isolateToMainStream = null;
_mainToIsolateStream = null;
_returnedEvent?.close();
_returnedEvent = null;
_isolate?.kill();
_isolate = null;
_isEngineInitialized = false;
_isInitialized = false;
SoLoudController().soLoudFFI.disposeAllSound();
SoLoudController().soLoudFFI.deinit();
_activeSounds.clear();
}
// //////////////////////////////////
// / isolate loop events management /
// //////////////////////////////////
/// Start the isolate loop to catch the end
/// of sounds (handles) playback or keys
///
/// The loop recursively call itself to check the state of
/// all active sound handles. Therefore it can cause some lag for
/// other event calls.
/// Not starting this will implies not receive [SoundEventType]s,
/// it will therefore be up to the developer to check
/// the sound handle validity
///
Future<bool> _startLoop() async {
_log.finest('_startLoop() called');
if (_isolate == null || !_isEngineInitialized) return false;
_mainToIsolateStream?.send(
{
'event': MessageEvents.startLoop,
'args': (),
},
);
await _waitForEvent(MessageEvents.startLoop, ());
return true;
}
// ////////////////////////////////////////////////
// Below all the methods implemented with FFI for the player
// ////////////////////////////////////////////////
/// Initialize the audio engine.
///
/// Defaults are:
/// Miniaudio audio backend
/// sample rate 44100
/// buffer 2048
// TODO(marco): add initialization parameters
Future<PlayerErrors> _initEngine() async {
_log.finest('_initEngine() called');
if (_isolate == null) {
throw StateError('The audio isolate is not running');
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.initEngine,
'args': (),
},
);
final ret =
await _waitForEvent(MessageEvents.initEngine, ()) as PlayerErrors;
_isEngineInitialized = ret == PlayerErrors.noError;
_logPlayerError(ret, from: '_initEngine() result');
/// start also the loop in the audio isolate
if (_isEngineInitialized) {
await _startLoop();
}
return ret;
}
/// Load a new sound to be played once or multiple times later, from
/// the file system.
///
/// Provide the complete [path] of the file to be played.
///
/// When [mode] is [LoadMode.memory], the whole uncompressed RAW PCM
/// audio is loaded into memory. Used to prevent gaps or lags
/// when seeking/starting a sound (less CPU, more memory allocated).
/// If [LoadMode.disk] is used instead, the audio data is loaded
/// from the given file when needed (more CPU, less memory allocated).
/// See the [seek] note problem when using [LoadMode.disk].
/// The default is [LoadMode.memory].
///
/// Returns the new sound as [AudioSource].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
///
/// If the file is already loaded, this is a no-op (but a warning
/// will be produced in the log).
Future<AudioSource> loadFile(
String path, {
LoadMode mode = LoadMode.memory,
}) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.loadFile,
'args': (completeFileName: path, mode: mode),
},
);
final ret = (await _waitForEvent(
MessageEvents.loadFile,
(completeFileName: path, mode: mode),
)) as ({PlayerErrors error, AudioSource? sound});
_logPlayerError(ret.error, from: 'loadFile() result');
if (ret.error == PlayerErrors.noError) {
assert(
ret.sound != null, 'loadFile() returned no sound despite no error');
_activeSounds.add(ret.sound!);
return ret.sound!;
} else if (ret.error == PlayerErrors.fileAlreadyLoaded) {
_log.warning(() => "Sound '$path' was already loaded. "
'Prefer loading only once, and reusing the loaded sound '
'when playing.');
// The `audio_isolate.dart` code has logic to find the already-loaded
// sound among active sounds. The sound should be here as well.
assert(
_activeSounds
.where((sound) => sound.soundHash == ret.sound!.soundHash)
.length ==
1,
'Sound is already loaded but missing from _activeSounds. '
'This is probably a bug in flutter_soloud, please file.');
return ret.sound!;
} else {
throw SoLoudCppException.fromPlayerError(ret.error);
}
}
/// Load a new sound to be played once or multiple times later, from
/// an asset.
///
/// Provide the [key] of the asset to load (e.g. `assets/sound.mp3`).
///
/// You can provide a custom [assetBundle]. By default, the [rootBundle]
/// is used.
///
/// Since SoLoud can only play from files, the asset will be copied to
/// a temporary file, and that file will be used to load the sound.
///
/// Throws a [FlutterError] if the asset is not found.
/// Throws a [SoLoudTemporaryFolderFailedException] if there was a problem
/// creating the temporary file that the asset will be copied to.
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
///
/// Returns the new sound as [AudioSource].
///
/// If the file is already loaded, this is a no-op (but a warning
/// will be produced in the log).
Future<AudioSource> loadAsset(
String key, {
LoadMode mode = LoadMode.memory,
AssetBundle? assetBundle,
}) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
final file = await _loader.loadAsset(key, assetBundle: assetBundle);
return loadFile(file.absolute.path, mode: mode);
}
/// Load a new sound to be played once or multiple times later, from
/// a network URL.
///
/// Provide the [url] of the sound to load.
///
/// Optionally, you can provide your own [httpClient]. This is a good idea
/// if you're loading several files in a short span of time (such as
/// on program startup). When no [httpClient] is provided,
/// a new one will be created (and closed afterwards) for each call.
///
/// Since SoLoud can only play from files, the downloaded data will be
/// copied to a temporary file, and that file will be used to load the sound.
///
/// Throws [FormatException] if the [url] is invalid.
/// Throws [SoLoudNetworkStatusCodeException] if the request fails
/// with a non-`200` status code.
/// Throws a [SoLoudTemporaryFolderFailedException] if there was a problem
/// creating the temporary file that the asset will be copied to.
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
///
/// Returns the new sound as [AudioSource].
///
/// If the file is already loaded, this is a no-op (but a warning
/// will be produced in the log).
Future<AudioSource> loadUrl(
String url, {
LoadMode mode = LoadMode.memory,
http.Client? httpClient,
}) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
final file = await _loader.loadUrl(url, httpClient: httpClient);
return loadFile(file.absolute.path, mode: mode);
}
/// Load a new waveform to be played once or multiple times later.
///
/// Specify the type of the waveform (such as sine or square or saw)
/// with [waveform].
///
/// You must also specify if the waveform should be a [superWave],
/// and what the superwave's [scale] and [detune] should be.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
///
/// Returns the new sound as [AudioSource].
Future<AudioSource> loadWaveform(
WaveForm waveform,
bool superWave,
double scale,
double detune,
) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.loadWaveform,
'args': (
waveForm: waveform.index,
superWave: superWave,
scale: scale,
detune: detune,
),
},
);
final ret = (await _waitForEvent(
MessageEvents.loadWaveform,
(
waveForm: waveform.index,
superWave: superWave,
scale: scale,
detune: detune,
),
)) as ({PlayerErrors error, AudioSource? sound});
if (ret.error == PlayerErrors.noError) {
_activeSounds.add(ret.sound!);
return ret.sound!;
}
_logPlayerError(ret.error, from: 'loadWaveform() result');
throw SoLoudCppException.fromPlayerError(ret.error);
}
/// Set a waveform type to the given sound: see [WaveForm] enum.
///
/// Provide the [sound] for which to change the waveform type,
/// and the new [newWaveform].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setWaveform(AudioSource sound, WaveForm newWaveform) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setWaveform(sound.soundHash, newWaveform);
}
/// If this sound is a `superWave` you can change the scale at runtime.
///
/// Provide the [sound] for which to change the scale,
/// and the new [newScale].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setWaveformScale(AudioSource sound, double newScale) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setWaveformScale(sound.soundHash, newScale);
}
/// If this sound is a `superWave` you can change the detune at runtime.
///
/// Provide the [sound] for which to change the detune,
/// and the new [newDetune].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setWaveformDetune(AudioSource sound, double newDetune) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setWaveformDetune(sound.soundHash, newDetune);
}
/// Set the frequency of the given waveform sound.
///
/// Provide the [sound] for which to change the scale,
/// and the new [newFrequency].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setWaveformFreq(AudioSource sound, double newFrequency) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setWaveformFreq(sound.soundHash, newFrequency);
}
/// Set the given waveform sound's super wave flag.
///
/// Provide the [sound] for which to change the flag,
/// and the new [superwave] value.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setWaveformSuperWave(AudioSource sound, bool superwave) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setWaveformSuperWave(
sound.soundHash,
superwave ? 1 : 0,
);
}
/// Create a new audio source from the given [textToSpeech].
///
/// Returns the new sound as [AudioSource].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
Future<AudioSource> speechText(String textToSpeech) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.speechText,
'args': (textToSpeech: textToSpeech),
},
);
final ret = (await _waitForEvent(
MessageEvents.speechText,
(textToSpeech: textToSpeech),
)) as ({PlayerErrors error, AudioSource sound});
_logPlayerError(ret.error, from: 'speechText() result');
if (ret.error == PlayerErrors.noError) {
_activeSounds.add(ret.sound);
return ret.sound;
}
throw SoLoudCppException.fromPlayerError(ret.error);
}
/// Play an already-loaded sound identified by [sound]. Creates a new
/// playing instance of the sound, and returns its [SoundHandle].
///
/// You can provide the [volume], where `1.0` is full volume and `0.0`
/// is silent. Defaults to `1.0`.
///
/// You can provide [pan] for the sound, with `0.0` centered,
/// `-1.0` fully left, and `1.0` fully right. Defaults to `0.0`.
///
/// Set [paused] to `true` if you want the new sound instance to
/// start paused. This is helpful if you want to change some attributes
/// of the sound instance before you play it. For example, you could
/// call [setRelativePlaySpeed] or [setProtectVoice] on the sound before
/// un-pausing it.
///
/// To play a looping sound, set [paused] to `true`. You can also
/// define the region to loop by setting [loopingStartAt]
/// (which defaults to the beginning of the sound otherwise).
/// There is no way to set the end of the looping region — it will
/// always be the end of the [sound].
///
/// Returns the [SoundHandle] of the new sound instance.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
Future<SoundHandle> play(
AudioSource sound, {
double volume = 1,
double pan = 0,
bool paused = false,
bool looping = false,
Duration loopingStartAt = Duration.zero,
}) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.play,
'args': (
soundHash: sound.soundHash,
volume: volume,
pan: pan,
paused: paused,
looping: looping,
loopingStartAt: loopingStartAt,
),
},
);
final ret = (await _waitForEvent(
MessageEvents.play,
(
soundHash: sound.soundHash,
volume: volume,
pan: pan,
paused: paused,
looping: looping,
loopingStartAt: loopingStartAt,
),
)) as ({PlayerErrors error, SoundHandle newHandle});
_logPlayerError(ret.error, from: 'play()');
if (ret.error != PlayerErrors.noError) {
throw SoLoudCppException.fromPlayerError(ret.error);
}
try {
/// add the new handle to the sound
_activeSounds
.firstWhere((s) => s.soundHash == sound.soundHash)
.handlesInternal
.add(ret.newHandle);
sound.handlesInternal.add(ret.newHandle);
} catch (e) {
_log.severe('play(): soundHash ${sound.soundHash} not found', e);
throw SoLoudSoundHashNotFoundDartException(sound.soundHash);
}
return ret.newHandle;
}
/// Pause or unpause a currently playing sound identified by [handle].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void pauseSwitch(SoundHandle handle) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.pauseSwitch(handle);
}
/// Pause or unpause a currently playing sound identified by [handle].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setPause(SoundHandle handle, bool pause) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setPause(handle, pause ? 1 : 0);
}
/// Gets the pause state of a currently playing sound identified by [handle].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
bool getPause(SoundHandle handle) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
return SoLoudController().soLoudFFI.getPause(handle);
}
/// Set a sound's relative play speed.
///
/// Provide the currently playing sound instance via its [handle],
/// and the new [speed].
///
/// Setting the speed value to `0` will cause undefined behavior,
/// likely a crash.
///
/// This changes the effective sample rate
/// while leaving the base sample rate alone.
/// Note that playing a sound at a higher sample rate will require SoLoud
/// to request more samples from the sound source, which will require more
/// memory and more processing power. Playing at a slower sample
/// rate is cheaper.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
void setRelativePlaySpeed(SoundHandle handle, double speed) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
SoLoudController().soLoudFFI.setRelativePlaySpeed(handle, speed);
}
/// Get a sound's relative play speed. Provide the sound instance via
/// its [handle].
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
double getRelativePlaySpeed(SoundHandle handle) {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
return SoLoudController().soLoudFFI.getRelativePlaySpeed(handle);
}
/// Stop a currently playing sound identified by [handle]
/// and clear it from the sound handle list.
///
/// This does _not_ dispose the audio source. Use [disposeSource] for that.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
Future<void> stop(SoundHandle handle) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{
'event': MessageEvents.stop,
'args': (handle: handle),
},
);
await _waitForEvent(MessageEvents.stop, (handle: handle));
/// find a sound with this handle and remove that handle from the list
for (final sound in _activeSounds) {
sound.handlesInternal.removeWhere((element) => element == handle);
if (sound.handles.isEmpty) {
sound.allInstancesFinishedController.add(null);
}
}
}
/// Stops all handles of the already loaded [source], and reclaims memory.
///
/// After an audio source has been disposed in this way,
/// do not attempt to play it.
///
/// Throws [SoLoudNotInitializedException] if the engine is not initialized.
Future<void> disposeSource(AudioSource source) async {
if (!isInitialized) {
throw const SoLoudNotInitializedException();
}
_mainToIsolateStream?.send(
{