diff --git a/.gitignore b/.gitignore index ee15be5..b18daed 100755 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,8 @@ images/ lib/flutter_soloud_FFIGEN.dart web/build +web/worker.dart.js.deps +web/worker.dart.js.map **/android/caches **/android/.tmp diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index d112e0a..7195ddf 100755 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -15,6 +15,20 @@ "intelliSenseMode": "linux-clang-x64", "configurationProvider": "ms-vscode.cmake-tools" }, + { + "name": "Chrome", + "includePath": [ + "${workspaceFolder}/**", + "${workspaceFolder}/src", + "/usr/lib/emscripten/system/include" + ], + "defines": ["WITH_MINIAUDIO", "DR_MP3_IMPLEMENTATION", "__EMSCRIPTEN__"], // to see the code in between "#if defined" + "compilerPath": "/usr/bin/clang", + "cStandard": "c17", + "cppStandard": "c++17", + "intelliSenseMode": "${default}", + "configurationProvider": "ms-vscode.cmake-tools" + }, { "name": "Win32", "includePath": [ diff --git a/.vscode/launch.json b/.vscode/launch.json index 8e76531..72ab720 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -78,6 +78,12 @@ "request": "launch", "program": "${workspaceFolder}/example/build/linux/x64/debug/bundle/flutter_soloud_example", "cwd": "${workspaceFolder}" + }, + { + "name": "Chrome", + "type": "chrome", + "preLaunchTask": "compile web debug", + "request": "launch" } ] } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3c6802d..729c81a 100755 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,49 +10,27 @@ { "label": "compile linux debug", "command": "cd ${workspaceFolder}/example; flutter build linux -t lib/main.dart --debug", - // "args": ["build", "linux"], "type": "shell" }, { "label": "compile linux test debug", "command": "cd ${workspaceFolder}/example; flutter build linux -t tests/tests.dart --debug", - // "args": ["build", "linux"], "type": "shell" }, { "label": "compile windows debug verbose", "command": "cd ${workspaceFolder}/example; flutter build windows -t lib/main.dart --debug --verbose", - // "args": ["build", "linux"], "type": "shell" }, { "label": "compile windows debug", "command": "cd ${workspaceFolder}/example; flutter build windows -t lib/main.dart --debug", - // "args": ["build", "linux"], "type": "shell" }, { - "type": "cppbuild", - "label": "C/C++: gcc build active file", - "command": "/usr/bin/gcc", - "args": [ - "-fdiagnostics-color=always", - "-g", - "${file}", - "-o", - "${fileDirname}/${fileBasenameNoExtension}" - ], - "options": { - "cwd": "${fileDirname}" - }, - "problemMatcher": [ - "$gcc" - ], - "group": { - "kind": "build", - "isDefault": true - }, - "detail": "Task generated by Debugger." + "label": "compile web debug", + "command": "cd ${workspaceFolder}/example; flutter run -d chrome --web-renderer canvaskit --web-browser-flag '--disable-web-security' -t lib/main.dart --release", + "type": "shell" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 51168a9..05d1f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ### 2.1.0 - added `getPan()`, `setPan()` and `setPanAbsolute()`. +- added support for the Web platform. +- added `loadMem()` to read the give audio file bytes buffer (not RAW data). Useful for the Web platform. +- fixed `getFilterParamNames()`. +- added `AudioData` class to manage audio samples. ### 2.0.2 (23 May 2024) - Fixed wrong exception raised by `setVolume()` when a handle is no more valid. diff --git a/README.md b/README.md index d4806c8..acb9b37 100755 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A low-level audio plugin for Flutter. |Linux|Windows|Android|MacOS|iOS|web| |:-:|:-:|:-:|:-:|:-:|:-:| -|πŸ’™|πŸ’™|πŸ’™|πŸ’™|πŸ’™| WIP | +|πŸ’™|πŸ’™|πŸ’™|πŸ’™|πŸ’™|πŸ’™| ### Select features: @@ -42,6 +42,7 @@ with the [miniaudio](https://miniaud.io/) backend through [Dart's C interop](https://dart.dev/interop/c-interop) (`dart:ffi`). In other words, it is calling the C/C++ methods of the underlying audio engine directly β€” there are no method channels in use. +To use this plugin on the **Web platform**, please refer to [WEB_NOTES](https://github.com/alnitak/flutter_soloud/blob/main/WEB_NOTES.md). ## Example diff --git a/WEB_NOTES.md b/WEB_NOTES.md new file mode 100644 index 0000000..31b945a --- /dev/null +++ b/WEB_NOTES.md @@ -0,0 +1,51 @@ +# Web Notes + + +## Description + +The web platform is now supported, but some testing is welcome. + +## How to use + +To add the plugin to a web app, add the following line to the `` section of `web/index.html`: +``` + +``` + +--- + +**`loadUrl()`** may produce the following error when the app is run: +>> Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3. (Reason: CORS header β€˜Access-Control-Allow-Origin’ missing). Status code: 200. + +This is due for the default beavior of http servers which don't allow to make requests outside their domain. Refer [here](https://enable-cors.org/server.html) to learn how to enable your server to handle this situation. +Instead, if you run the app locally, you could run the app with something like the following command: +``` +flutter run -d chrome --web-renderer canvaskit --web-browser-flag '--disable-web-security' -t lib/main.dart --release +``` + +--- + +***It is not possible to read a local audio file directly*** on the web. For this reason, `loadMem()` has been added, which requires the `Uint8List` byte buffer of the audio file. +***IMPORTANT***: `loadMem()` with mode `LoadMode.memory` used on web platform will freeze the UI for the time needed to decompress the audio file. Please use it with mode `LoadMode.disk` or load your sound when the app starts. + +## For developers + +In the `web` directory, there is a `compile_wasm.sh` script that generates the `.js` and `.wasm` files for the native C code located in the `src` dir. Run it after installing *emscripten*. There is also a `compile_web.sh` to compile the web worker needed by native code to communicate with Dart. The generated files are already provided, but if it is needed to modify C/C++ code or the `web/worker.dart` code, the scripts must be run to reflect the changes. + +The `compile_wasm.sh` script uses the `-O3` code optimization flag. +To see a better errors logs, use `-O0 -g -s ASSERTIONS=1` in `compile_wasm.sh`. + +--- + +The `AudioIsolate` [has been removed](https://github.com/alnitak/flutter_soloud/pull/89) and all the logic has been implemented natively. Events like `voice ended` are sent from C back to Dart. However, since it is not possible to call Dart from a native thread (the audio thread), a new web worker is created using the WASM `EM_ASM` directive. This allows sending the `voice ended` event back to Dart via the worker. + +Here a sketch to show the step used: +![sketch](img/wasmWorker.png) + +**#1.** This function is called while initializing the player with `FlutterSoLoudWeb.setDartEventCallbacks()`. +It creates a Web Worker in the [WASM Module](https://emscripten.org/docs/api_reference/module.html) using the compiled `web/worker.dart`. After calling this, the WASM Module will have a new variable called `Module.wasmWorker` which will be used in Dart to receive messages. +By doing this it will be easy to use the Worker to send messages from within the CPP code. +**#2.** This function, like #1, uses [EM_ASM](https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-call-javascript-from-native) to inline JS. This JS code uses the `Module.wasmWorker` created in #1 to send a message. +**#3.** This is the JS used and created in #1. Every messages sent by #2 are managed here and sent to #4. +**#4.** Here when the event message has been received, a new event is added to a Stream. This Stream is listened by the SoLoud API. +**#5.** Here we listen to the event messages coming from the `WorkerController` stream. Currently, only the "voice ended" event is supported. The Stream is listened in `SoLoud._initializeNativeCallbacks()`. diff --git a/example/.metadata b/example/.metadata index 730798a..68b331f 100644 --- a/example/.metadata +++ b/example/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - channel: stable + revision: "761747bfc538b5af34aa0d3fac380f1bc331ec49" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: windows - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + - platform: web + create_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 + base_revision: 761747bfc538b5af34aa0d3fac380f1bc331ec49 # User provided section diff --git a/example/assets/audio/Tropical Beeper.mp3 b/example/assets/audio/TropicalBeeper.mp3 similarity index 100% rename from example/assets/audio/Tropical Beeper.mp3 rename to example/assets/audio/TropicalBeeper.mp3 diff --git a/example/assets/audio/X trackTure.mp3 b/example/assets/audio/XtrackTure.mp3 similarity index 100% rename from example/assets/audio/X trackTure.mp3 rename to example/assets/audio/XtrackTure.mp3 diff --git a/example/assets/shaders/test9.frag b/example/assets/shaders/test9.frag index 5b80c31..8cd31a8 100644 --- a/example/assets/shaders/test9.frag +++ b/example/assets/shaders/test9.frag @@ -174,11 +174,11 @@ void mainImage( out vec4 fragColor, in vec2 fragCoord ) // rotate view float a; - a = -0.6; + a = -0.8; rd = rotateX(rd, a); ro = rotateX(ro, a); - a = 1.5; + a = 1.8; // a = sin(iTime)*.5 + 1.570796327; rd = rotateY(rd, a); ro = rotateY(ro, a); diff --git a/example/lib/controls.dart b/example/lib/controls.dart index fce548e..2eb8da6 100644 --- a/example/lib/controls.dart +++ b/example/lib/controls.dart @@ -1,6 +1,6 @@ import 'dart:async'; -import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -29,8 +29,8 @@ class _ControlsState extends State { // ignore: avoid_positional_boolean_parameters ButtonStyle buttonStyle(bool enabled) { return enabled - ? ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.green)) - : ButtonStyle(backgroundColor: MaterialStateProperty.all(Colors.red)); + ? ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.green)) + : ButtonStyle(backgroundColor: WidgetStateProperty.all(Colors.red)); } @override @@ -49,7 +49,8 @@ class _ControlsState extends State { ElevatedButton( onPressed: () async { /// Ask recording permission on mobile - if (Platform.isAndroid || Platform.isIOS) { + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS) { final p = await Permission.microphone.isGranted; if (!p) { unawaited(Permission.microphone.request()); @@ -76,9 +77,8 @@ class _ControlsState extends State { blurSigmaX: 6, blurSigmaY: 6, ), - linearShapeParams: LinearShapeParams( + linearShapeParams: const LinearShapeParams( angle: -90, - space: Platform.isAndroid || Platform.isIOS ? -10 : 10, alignment: LinearAlignment.left, ), ), diff --git a/example/lib/main.dart b/example/lib/main.dart index a0d2496..cbc0e62 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -82,7 +82,6 @@ class MyHomePage extends StatelessWidget { Widget build(BuildContext context) { return DefaultTabController( length: 5, - initialIndex: 2, child: SafeArea( child: Scaffold( body: Column( diff --git a/example/lib/page_3d_audio.dart b/example/lib/page_3d_audio.dart index 51f176f..33f84e2 100644 --- a/example/lib/page_3d_audio.dart +++ b/example/lib/page_3d_audio.dart @@ -70,11 +70,12 @@ class _Page3DAudioState extends State { /// load the audio file currentSound = await SoLoud.instance.loadUrl( - 'https://www.learningcontainer.com/wp-content/uploads/2020/02/Kalimba.mp3', + // From https://freetestdata.com/audio-files/mp3/ + 'https://marcobavagnoli.com/Free_Test_Data_500KB_MP3.mp3', ); /// play it - await SoLoud.instance.play3d(currentSound!, 0, 0, 0); + await SoLoud.instance.play3d(currentSound!, 0, 0, 0, looping: true); spinAround = true; diff --git a/example/lib/page_hello_flutter.dart b/example/lib/page_hello_flutter.dart index 1ed3a77..39c9f2e 100644 --- a/example/lib/page_hello_flutter.dart +++ b/example/lib/page_hello_flutter.dart @@ -1,11 +1,11 @@ import 'dart:async'; -import 'dart:ffi' as ffi; -import 'dart:typed_data'; +import 'dart:io'; -import 'package:ffi/ffi.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:logging/logging.dart'; @@ -31,6 +31,26 @@ class _PageHelloFlutterSoLoudState extends State { child: Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ + /// pick audio file + ElevatedButton( + onPressed: () async { + final paths = (await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['mp3', 'wav', 'ogg', 'flac'], + onFileLoading: print, + dialogTitle: 'Pick audio file\n(not for web)', + )) + ?.files; + if (paths != null) { + unawaited(playFile(paths.first.path!)); + } + }, + child: const Text( + 'pick audio\n(not for web)', + textAlign: TextAlign.center, + ), + ), + /// pick audio file ElevatedButton( onPressed: () async { @@ -41,11 +61,21 @@ class _PageHelloFlutterSoLoudState extends State { dialogTitle: 'Pick audio file', )) ?.files; + if (paths != null) { - unawaited(play(paths.first.path!)); + if (kIsWeb) { + unawaited(playBuffer(paths.first.name, paths.first.bytes!)); + } else { + final f = File(paths.first.path!); + final buffer = f.readAsBytesSync(); + unawaited(playBuffer(paths.first.path!, buffer)); + } } }, - child: const Text('pick audio'), + child: const Text( + 'pick audio using "loadMem()"\n(all platforms)', + textAlign: TextAlign.center, + ), ), Column( children: [ @@ -56,7 +86,7 @@ class _PageHelloFlutterSoLoudState extends State { SoLoudCapture.instance.stopCapture(); if (context.mounted) setState(() {}); } else { - final a = SoLoudCapture.instance.initialize(); + final a = SoLoudCapture.instance.init(); final b = SoLoudCapture.instance.startCapture(); if (context.mounted && a == CaptureErrors.captureNoError && @@ -82,7 +112,7 @@ class _PageHelloFlutterSoLoudState extends State { } /// play file - Future play(String file) async { + Future playFile(String file) async { /// stop any previous sound loaded if (currentSound != null) { try { @@ -107,6 +137,33 @@ class _PageHelloFlutterSoLoudState extends State { /// play it await SoLoud.instance.play(currentSound!); } + + /// play bytes for web. + Future playBuffer(String fileName, Uint8List bytes) async { + /// stop any previous sound loaded + if (currentSound != null) { + try { + await SoLoud.instance.disposeSource(currentSound!); + } catch (e) { + _log.severe('dispose error', e); + return; + } + } + + /// load the audio file + final AudioSource newSound; + try { + newSound = await SoLoud.instance.loadMem(fileName, bytes); + } catch (e) { + _log.severe('load error', e); + return; + } + + currentSound = newSound; + + /// play it + await SoLoud.instance.play(currentSound!); + } } /// widget that uses a ticker to read and provide audio @@ -127,27 +184,32 @@ class MicAudioWidget extends StatefulWidget { class _MicAudioWidgetState extends State with SingleTickerProviderStateMixin { - late Ticker ticker; - late ffi.Pointer> audioData; + Ticker? ticker; + final audioData = AudioData( + GetSamplesFrom.microphone, + GetSamplesKind.wave, + ); @override void initState() { super.initState(); - audioData = calloc(); - SoLoudCapture.instance.getCaptureAudioTexture2D(audioData); ticker = createTicker((Duration elapsed) { - if (mounted) { - SoLoudCapture.instance.getCaptureAudioTexture2D(audioData); - setState(() {}); + if (context.mounted) { + try { + audioData.updateSamples(); + setState(() {}); + } on Exception catch (e) { + debugPrint('$e'); + } } }); - ticker.start(); + ticker?.start(); } @override void dispose() { - ticker.stop(); - calloc.free(audioData); + ticker?.stop(); + audioData.dispose(); super.dispose(); } @@ -168,7 +230,7 @@ class MicAudioPainter extends CustomPainter { const MicAudioPainter({ required this.audioData, }); - final ffi.Pointer> audioData; + final AudioData audioData; @override void paint(Canvas canvas, Size size) { @@ -188,9 +250,7 @@ class MicAudioPainter extends CustomPainter { for (var n = 0; n < 32; n++) { var f = 0.0; for (var i = 0; i < 8; i++) { - /// audioData[n * 8 + i] is the FFT data - /// If you want wave data, add 256 to the index - f += audioData.value[n * 8 + i + 256]; + f += audioData.getWave(SampleWave(n * 8 + i)); } data[n] = f / 8; } diff --git a/example/lib/page_multi_track.dart b/example/lib/page_multi_track.dart index 25a1fae..dbb27e8 100644 --- a/example/lib/page_multi_track.dart +++ b/example/lib/page_multi_track.dart @@ -1,12 +1,9 @@ // ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; class PageMultiTrack extends StatefulWidget { const PageMultiTrack({super.key}); @@ -175,15 +172,8 @@ class _PlaySoundWidgetState extends State { } Future loadAsset() async { - final path = (await getAssetFile(widget.assetsAudio)).path; - final AudioSource? newSound; - try { - newSound = await SoLoud.instance.loadFile(path); - } catch (e) { - _log.severe('Load sound asset failed', e); - return false; - } + newSound = await SoLoud.instance.loadAsset(widget.assetsAudio); soundLength = SoLoud.instance.getLength(newSound); sound = newSound; @@ -260,24 +250,6 @@ class _PlaySoundWidgetState extends State { isPaused[newHandle] = ValueNotifier(false); soundPosition[newHandle] = ValueNotifier(0); } - - /// get the assets file and copy it to the temp dir - Future getAssetFile(String assetsFile) async { - final tempDir = await getTemporaryDirectory(); - final tempPath = tempDir.path; - final filePath = '$tempPath/$assetsFile'; - final file = File(filePath); - if (file.existsSync()) { - return file; - } else { - final byteData = await rootBundle.load(assetsFile); - final buffer = byteData.buffer; - await file.create(recursive: true); - return file.writeAsBytes( - buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), - ); - } - } } /// row widget containing play/pause and time slider diff --git a/example/lib/page_visualizer.dart b/example/lib/page_visualizer.dart index 27d645b..16a04b6 100644 --- a/example/lib/page_visualizer.dart +++ b/example/lib/page_visualizer.dart @@ -1,15 +1,12 @@ import 'dart:async'; -import 'dart:io'; -import 'dart:ui' as ui; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; import 'package:flutter_soloud_example/controls.dart'; import 'package:flutter_soloud_example/visualizer/visualizer.dart'; import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:star_menu/star_menu.dart'; class PageVisualizer extends StatefulWidget { @@ -40,19 +37,35 @@ class _PageVisualizerState extends State { 'assets/audio/12Bands/audiocheck.net_sin_16000Hz_-3dBFS_2s.wav', 'assets/audio/12Bands/audiocheck.net_sin_20000Hz_-3dBFS_2s.wav', ]; - final ValueNotifier textureType = - ValueNotifier(TextureType.fft2D); + late final ValueNotifier samplesKind; final ValueNotifier fftSmoothing = ValueNotifier(0.8); final ValueNotifier isVisualizerForPlayer = ValueNotifier(true); final ValueNotifier isVisualizerEnabled = ValueNotifier(true); - final ValueNotifier fftImageRange = - ValueNotifier(const RangeValues(0, 255)); - final ValueNotifier maxFftImageRange = ValueNotifier(255); + late ValueNotifier fftImageRange; final ValueNotifier soundLength = ValueNotifier(0); final ValueNotifier soundPosition = ValueNotifier(0); Timer? timer; AudioSource? currentSound; - FftController visualizerController = FftController(); + late final VisualizerController visualizerController; + + @override + void initState() { + super.initState(); + samplesKind = ValueNotifier(GetSamplesKind.linear); + visualizerController = VisualizerController(samplesKind: samplesKind.value); + fftImageRange = ValueNotifier( + RangeValues( + visualizerController.minRange.toDouble(), + visualizerController.maxRange.toDouble(), + ), + ); + } + + @override + void dispose() { + visualizerController.audioData.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -66,14 +79,14 @@ class _PageVisualizerState extends State { SoLoudCapture.instance.stopCapture(); visualizerController.changeIsCaptureStarted(false); } else { - SoLoudCapture.instance.initialize(deviceID: deviceID); + SoLoudCapture.instance.init(deviceID: deviceID); SoLoudCapture.instance.startCapture(); visualizerController.changeIsCaptureStarted(true); } }, onDeviceIdChanged: (deviceID) { SoLoudCapture.instance.stopCapture(); - SoLoudCapture.instance.initialize(deviceID: deviceID); + SoLoudCapture.instance.init(deviceID: deviceID); SoLoudCapture.instance.startCapture(); }, ), @@ -93,7 +106,10 @@ class _PageVisualizerState extends State { ), linearShapeParams: LinearShapeParams( angle: -90, - space: Platform.isAndroid || Platform.isIOS ? -10 : 10, + space: defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS + ? -10 + : 10, alignment: LinearAlignment.left, ), ), @@ -111,14 +127,14 @@ class _PageVisualizerState extends State { ActionChip( backgroundColor: Colors.blue, onPressed: () { - playAsset('assets/audio/Tropical Beeper.mp3'); + playAsset('assets/audio/TropicalBeeper.mp3'); }, label: const Text('Tropical Beeper'), ), ActionChip( backgroundColor: Colors.blue, onPressed: () { - playAsset('assets/audio/X trackTure.mp3'); + playAsset('assets/audio/XtrackTure.mp3'); }, label: const Text('X trackTure'), ), @@ -179,7 +195,7 @@ class _PageVisualizerState extends State { ), const SizedBox(width: 10), - /// texture type + /// texture kind StarMenu( params: StarMenuParameters( shape: MenuShape.linear, @@ -198,48 +214,52 @@ class _PageVisualizerState extends State { controller.closeMenu!(); }, items: [ - /// frequencies on 1st 256 px row - /// wave on 2nd 256 px row + /// wave data (amplitudes) ActionChip( backgroundColor: Colors.blue, onPressed: () { - textureType.value = TextureType.both1D; + samplesKind.value = GetSamplesKind.wave; + visualizerController + .changeSamplesKind(GetSamplesKind.wave); + fftImageRange.value = const RangeValues(0, 255); }, - label: const Text('both 1D'), + label: const Text('wave data'), ), - /// frequencies (FFT) + /// frequencies on 1st 256 px row + /// wave on 2nd 256 px row ActionChip( backgroundColor: Colors.blue, onPressed: () { - textureType.value = TextureType.fft2D; + samplesKind.value = GetSamplesKind.linear; + visualizerController + .changeSamplesKind(GetSamplesKind.linear); + fftImageRange.value = const RangeValues(0, 255); }, - label: const Text('frequencies'), + label: const Text('linear'), ), - /// wave data (amplitudes) + /// both fft and wave ActionChip( backgroundColor: Colors.blue, onPressed: () { - textureType.value = TextureType.wave2D; + samplesKind.value = GetSamplesKind.texture; + visualizerController + .changeSamplesKind(GetSamplesKind.texture); + fftImageRange.value = const RangeValues(0, 511); }, - label: const Text('wave data'), + label: const Text('texture'), ), - - /// both fft and wave - /// not implemented yet - // ActionChip( - // backgroundColor: Colors.blue, - // onPressed: () { - // textureType.value = TextureType.both2D; - // }, - // label: const Text('both'), - // ), ], - child: const Chip( - label: Text('texture'), - backgroundColor: Colors.blue, - avatar: Icon(Icons.arrow_drop_down), + child: ValueListenableBuilder( + valueListenable: samplesKind, + builder: (_, type, __) { + return Chip( + label: Text(type.name), + backgroundColor: Colors.blue, + avatar: const Icon(Icons.arrow_drop_down), + ); + }, ), ), ], @@ -270,8 +290,17 @@ class _PageVisualizerState extends State { dialogTitle: 'Pick audio file', )) ?.files; + if (paths != null) { - unawaited(play(paths.first.path!)); + final AudioSource audioFile; + if (kIsWeb) { + audioFile = await SoLoud.instance + .loadMem(paths.first.name, paths.first.bytes!); + } else { + audioFile = + await SoLoud.instance.loadFile(paths.first.path!); + } + unawaited(play(audioFile)); } }, child: const Text('pick audio'), @@ -279,47 +308,51 @@ class _PageVisualizerState extends State { ], ), - /// Seek slider - ValueListenableBuilder( - valueListenable: soundLength, - builder: (_, length, __) { - return ValueListenableBuilder( - valueListenable: soundPosition, - builder: (_, position, __) { - if (position >= length) { - position = 0; - if (length == 0) length = 1; - } - - return Row( - children: [ - Text(position.toInt().toString()), - Expanded( - child: Slider.adaptive( - value: position, - max: length < position ? position : length, - onChanged: (value) { - if (currentSound == null) return; - stopTimer(); - final position = Duration( - milliseconds: - (value * Duration.millisecondsPerSecond) - .round(), - ); - SoLoud.instance - .seek(currentSound!.handles.last, position); - soundPosition.value = value; - startTimer(); - }, + /// Seek slider. + /// Not used on web platforms because [LoadMode.disk] + /// is used with `loadMem()`. Otherwise the seek problem will + /// be noticeable while seeking. See [SoLoud.seek] note. + if (!kIsWeb) + ValueListenableBuilder( + valueListenable: soundLength, + builder: (_, length, __) { + return ValueListenableBuilder( + valueListenable: soundPosition, + builder: (_, position, __) { + if (position >= length) { + position = 0; + if (length == 0) length = 1; + } + + return Row( + children: [ + Text(position.toInt().toString()), + Expanded( + child: Slider.adaptive( + value: position, + max: length < position ? position : length, + onChanged: (value) { + if (currentSound == null) return; + stopTimer(); + final position = Duration( + milliseconds: + (value * Duration.millisecondsPerSecond) + .round(), + ); + SoLoud.instance + .seek(currentSound!.handles.last, position); + soundPosition.value = value; + startTimer(); + }, + ), ), - ), - Text(length.toInt().toString()), - ], - ); - }, - ); - }, - ), + Text(length.toInt().toString()), + ], + ); + }, + ); + }, + ), /// fft range slider values to put into the texture ValueListenableBuilder( @@ -330,14 +363,13 @@ class _PageVisualizerState extends State { Text('FFT range ${fftRange.start.toInt()}'), Expanded( child: RangeSlider( - max: 255, - divisions: 256, + max: visualizerController.maxRangeLimit.toDouble() + 1, values: fftRange, onChanged: (values) { fftImageRange.value = values; visualizerController - ..changeMinFreq(values.start.toInt()) - ..changeMaxFreq(values.end.toInt()); + ..changeMin(values.start.toInt()) + ..changeMax(values.end.toInt()); }, ), ), @@ -387,7 +419,7 @@ class _PageVisualizerState extends State { .changeIsVisualizerForPlayer(!value); }, ), - const Text('show capture data'), + const Text('show mic data'), Checkbox( value: forPlayer, onChanged: (value) { @@ -401,53 +433,11 @@ class _PageVisualizerState extends State { }, ), - /// switch to enable / disable retrieving audio data - ValueListenableBuilder( - valueListenable: isVisualizerEnabled, - builder: (_, isEnabled, __) { - return Row( - children: [ - Switch( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: isEnabled, - onChanged: (value) { - isVisualizerEnabled.value = value; - visualizerController.changeIsVisualizerEnabled(value); - }, - ), - const Text('FFT data'), - ], - ); - }, - ), - /// VISUALIZER - FutureBuilder( - future: loadShader(), - builder: (context, snapshot) { - if (snapshot.hasData) { - return ValueListenableBuilder( - valueListenable: textureType, - builder: (_, type, __) { - return Visualizer( - key: UniqueKey(), - controller: visualizerController, - shader: snapshot.data!, - textureType: type, - ); - }, - ); - } else { - if (snapshot.data == null) { - return const Placeholder( - child: Align( - child: Text('Error compiling shader.\nSee log'), - ), - ); - } - return const CircularProgressIndicator(); - } - }, + Visualizer( + // key: UniqueKey(), + controller: visualizerController, + shader: shader, ), ], ), @@ -455,19 +445,8 @@ class _PageVisualizerState extends State { ); } - /// load asynchronously the fragment shader - Future loadShader() async { - try { - final program = await ui.FragmentProgram.fromAsset(shader); - return program.fragmentShader(); - } catch (e) { - _log.severe('error compiling the shader', e); - } - return null; - } - /// play file - Future play(String file) async { + Future play(AudioSource source) async { if (currentSound != null) { try { await SoLoud.instance.disposeSource(currentSound!); @@ -477,9 +456,7 @@ class _PageVisualizerState extends State { } stopTimer(); } - - /// load the file - currentSound = await SoLoud.instance.loadFile(file); + currentSound = source; /// play it await SoLoud.instance.play(currentSound!); @@ -494,10 +471,12 @@ class _PageVisualizerState extends State { (event) { stopTimer(); - /// It's needed to call dispose when it end else it will + /// It's needed to call dispose when it ends else it will /// not be cleared - SoLoud.instance.disposeSource(currentSound!); - currentSound = null; + if (currentSound != null) { + SoLoud.instance.disposeSource(currentSound!); + currentSound = null; + } }, ); startTimer(); @@ -505,26 +484,12 @@ class _PageVisualizerState extends State { /// plays an assets file Future playAsset(String assetsFile) async { - final audioFile = await getAssetFile(assetsFile); - return play(audioFile.path); - } - - /// get the assets file and copy it to the temp dir - Future getAssetFile(String assetsFile) async { - final tempDir = await getTemporaryDirectory(); - final tempPath = tempDir.path; - final filePath = '$tempPath/$assetsFile'; - final file = File(filePath); - if (file.existsSync()) { - return file; - } else { - final byteData = await rootBundle.load(assetsFile); - final buffer = byteData.buffer; - await file.create(recursive: true); - return file.writeAsBytes( - buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), - ); - } + // final audioFile = await getAssetFile(assetsFile); + final audioFile = await SoLoud.instance.loadAsset( + assetsFile, + mode: kIsWeb ? LoadMode.disk : LoadMode.memory, + ); + return play(audioFile); } /// start timer to update the audio position slider diff --git a/example/lib/page_waveform.dart b/example/lib/page_waveform.dart index 7f7ce77..3e13a60 100644 --- a/example/lib/page_waveform.dart +++ b/example/lib/page_waveform.dart @@ -1,12 +1,10 @@ -import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; -import 'package:flutter_soloud_example/waveform/bars.dart'; -import 'package:flutter_soloud_example/waveform/filter_fx.dart'; -import 'package:flutter_soloud_example/waveform/keyboard_widget.dart'; -import 'package:flutter_soloud_example/waveform/knobs_groups.dart'; -import 'package:flutter_soloud_example/waveform/text_slider.dart'; +import 'package:flutter_soloud_example/ui/bars.dart'; +import 'package:flutter_soloud_example/ui/filter_fx.dart'; +import 'package:flutter_soloud_example/ui/keyboard_widget.dart'; +import 'package:flutter_soloud_example/ui/knobs_groups.dart'; +import 'package:flutter_soloud_example/ui/text_slider.dart'; import 'package:star_menu/star_menu.dart'; /// Example to demostrate how waveforms work with a keyboard @@ -82,13 +80,8 @@ class _PageWaveformState extends State { await SoLoud.instance.disposeSource(sound!); } - /// text created by ChatGPT :) await SoLoud.instance - .speechText('Flutter and So Loud audio plugin are the ' - "tech tag team you never knew you needed - they're " - 'like Batman and Robin, swooping in to save your ' - 'app with style and sound effects that would make ' - 'even Gotham jealous!') + .speechText('Hello Flutter Soloud!') .then((value) => sound = value); }, child: const Text('T2S'), @@ -290,9 +283,8 @@ class _PageWaveformState extends State { blurSigmaX: 6, blurSigmaY: 6, ), - linearShapeParams: LinearShapeParams( + linearShapeParams: const LinearShapeParams( angle: -90, - space: Platform.isAndroid || Platform.isIOS ? -10 : 10, alignment: LinearAlignment.left, ), ), diff --git a/example/lib/waveform/bars.dart b/example/lib/ui/bars.dart similarity index 76% rename from example/lib/waveform/bars.dart rename to example/lib/ui/bars.dart index e2d49aa..37465be 100644 --- a/example/lib/waveform/bars.dart +++ b/example/lib/ui/bars.dart @@ -1,6 +1,3 @@ -import 'dart:ffi' as ffi; - -import 'package:ffi/ffi.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; @@ -8,8 +5,9 @@ import 'package:flutter_soloud_example/visualizer/bars_fft_widget.dart'; import 'package:flutter_soloud_example/visualizer/bars_wave_widget.dart'; /// Visualizer for FFT and wave data -/// class Bars extends StatefulWidget { + /// If true get audio data from the player else from the mic + const Bars({super.key}); @override @@ -18,12 +16,13 @@ class Bars extends StatefulWidget { class BarsState extends State with SingleTickerProviderStateMixin { late final Ticker ticker; - ffi.Pointer> playerData = ffi.nullptr; - + final AudioData audioData = AudioData( + GetSamplesFrom.player, + GetSamplesKind.linear, + ); @override void initState() { super.initState(); - playerData = calloc(); ticker = createTicker(_tick); ticker.start(); } @@ -31,15 +30,18 @@ class BarsState extends State with SingleTickerProviderStateMixin { @override void dispose() { ticker.stop(); - calloc.free(playerData); - playerData = ffi.nullptr; + audioData.dispose(); super.dispose(); } void _tick(Duration elapsed) { - if (mounted) { - SoLoud.instance.getAudioTexture2D(playerData); - setState(() {}); + if (context.mounted) { + try { + audioData.updateSamples(); + setState(() {}); + } on Exception catch (e) { + debugPrint('$e'); + } } } @@ -50,7 +52,7 @@ class BarsState extends State with SingleTickerProviderStateMixin { child: Row( children: [ BarsFftWidget( - audioData: playerData.value, + audioData: audioData, minFreq: 0, maxFreq: 128, width: MediaQuery.sizeOf(context).width / 2 - 17, @@ -58,7 +60,7 @@ class BarsState extends State with SingleTickerProviderStateMixin { ), const SizedBox(width: 6), BarsWaveWidget( - audioData: playerData.value, + audioData: audioData, width: MediaQuery.sizeOf(context).width / 2 - 17, height: MediaQuery.sizeOf(context).width / 6, ), diff --git a/example/lib/waveform/filter_fx.dart b/example/lib/ui/filter_fx.dart similarity index 97% rename from example/lib/waveform/filter_fx.dart rename to example/lib/ui/filter_fx.dart index 99c66f1..c768e18 100644 --- a/example/lib/waveform/filter_fx.dart +++ b/example/lib/ui/filter_fx.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; -import 'package:flutter_soloud_example/waveform/touch_slider.dart'; +import 'package:flutter_soloud_example/ui/touch_slider.dart'; class FilterFx extends StatefulWidget { const FilterFx({ @@ -9,6 +9,7 @@ class FilterFx extends StatefulWidget { }); final FilterType filterType; + @override State createState() => _FilterFxState(); } diff --git a/example/lib/waveform/keyboard_widget.dart b/example/lib/ui/keyboard_widget.dart similarity index 100% rename from example/lib/waveform/keyboard_widget.dart rename to example/lib/ui/keyboard_widget.dart diff --git a/example/lib/waveform/knobs_groups.dart b/example/lib/ui/knobs_groups.dart similarity index 95% rename from example/lib/waveform/knobs_groups.dart rename to example/lib/ui/knobs_groups.dart index db2b71b..07a9cab 100644 --- a/example/lib/waveform/knobs_groups.dart +++ b/example/lib/ui/knobs_groups.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_soloud_example/waveform/touch_slider.dart'; +import 'package:flutter_soloud_example/ui/touch_slider.dart'; class KnobsGroup extends StatefulWidget { const KnobsGroup({ diff --git a/example/lib/waveform/text_slider.dart b/example/lib/ui/text_slider.dart similarity index 100% rename from example/lib/waveform/text_slider.dart rename to example/lib/ui/text_slider.dart diff --git a/example/lib/waveform/touch_slider.dart b/example/lib/ui/touch_slider.dart similarity index 100% rename from example/lib/waveform/touch_slider.dart rename to example/lib/ui/touch_slider.dart diff --git a/example/lib/visualizer/bars_fft_widget.dart b/example/lib/visualizer/bars_fft_widget.dart index 3801c32..85920af 100644 --- a/example/lib/visualizer/bars_fft_widget.dart +++ b/example/lib/visualizer/bars_fft_widget.dart @@ -1,7 +1,8 @@ -import 'dart:ffi' as ffi; - +// ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:flutter_soloud/flutter_soloud.dart'; + /// Draw the audio FFT data /// class BarsFftWidget extends StatelessWidget { @@ -14,7 +15,7 @@ class BarsFftWidget extends StatelessWidget { super.key, }); - final ffi.Pointer audioData; + final AudioData audioData; final int minFreq; final int maxFreq; final double width; @@ -22,7 +23,9 @@ class BarsFftWidget extends StatelessWidget { @override Widget build(BuildContext context) { - if (audioData.address == 0x0) return const SizedBox.shrink(); + if (audioData.getSamplesKind == GetSamplesKind.wave) { + return const Placeholder(); + } return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -55,20 +58,31 @@ class FftPainter extends CustomPainter { required this.minFreq, required this.maxFreq, }); - final ffi.Pointer audioData; + final AudioData audioData; final int minFreq; final int maxFreq; @override void paint(Canvas canvas, Size size) { - final barWidth = size.width / (maxFreq - minFreq); + final barWidth = size.width / (maxFreq - minFreq).clamp(0, 255); final paint = Paint() ..color = Colors.yellow ..strokeWidth = barWidth * 0.8 ..style = PaintingStyle.stroke; - for (var i = minFreq; i <= maxFreq; i++) { - final barHeight = size.height * audioData[i]; + for (var i = minFreq; i <= maxFreq.clamp(0, 255); i++) { + late final double barHeight; + try { + final double data; + if (audioData.getSamplesKind == GetSamplesKind.linear) { + data = audioData.getLinearFft(SampleLinear(i)); + } else { + data = audioData.getTexture(SampleRow(0), SampleColumn(i)); + } + barHeight = size.height * data; + } on Exception { + barHeight = 0; + } canvas.drawRect( Rect.fromLTWH( barWidth * (i - minFreq), diff --git a/example/lib/visualizer/bars_wave_widget.dart b/example/lib/visualizer/bars_wave_widget.dart index 0967064..14250c1 100644 --- a/example/lib/visualizer/bars_wave_widget.dart +++ b/example/lib/visualizer/bars_wave_widget.dart @@ -1,6 +1,5 @@ -import 'dart:ffi' as ffi; - import 'package:flutter/material.dart'; +import 'package:flutter_soloud/flutter_soloud.dart'; /// Draw the audio wave data /// @@ -12,14 +11,12 @@ class BarsWaveWidget extends StatelessWidget { super.key, }); - final ffi.Pointer audioData; + final AudioData audioData; final double width; final double height; @override Widget build(BuildContext context) { - if (audioData.address == 0x0) return const SizedBox.shrink(); - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -45,7 +42,7 @@ class WavePainter extends CustomPainter { const WavePainter({ required this.audioData, }); - final ffi.Pointer audioData; + final AudioData audioData; @override void paint(Canvas canvas, Size size) { @@ -56,7 +53,20 @@ class WavePainter extends CustomPainter { ..style = PaintingStyle.stroke; for (var i = 0; i < 256; i++) { - final barHeight = size.height * audioData[i + 256]; + late final double barHeight; + try { + final double data; + if (audioData.getSamplesKind == GetSamplesKind.wave) { + data = audioData.getWave(SampleWave(i)); + } else if (audioData.getSamplesKind == GetSamplesKind.linear) { + data = audioData.getLinearWave(SampleLinear(i)); + } else { + data = audioData.getTexture(SampleRow(0), SampleColumn(i + 256)); + } + barHeight = size.height * data; + } on Exception { + barHeight = 0; + } canvas.drawRect( Rect.fromLTWH( barWidth * i, diff --git a/example/lib/visualizer/visualizer.dart b/example/lib/visualizer/visualizer.dart index 23c0ce2..922c777 100644 --- a/example/lib/visualizer/visualizer.dart +++ b/example/lib/visualizer/visualizer.dart @@ -1,11 +1,9 @@ // ignore_for_file: avoid_positional_boolean_parameters import 'dart:async'; -import 'dart:ffi' as ffi; -import 'dart:typed_data'; import 'dart:ui' as ui; -import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; @@ -15,49 +13,52 @@ import 'package:flutter_soloud_example/visualizer/bars_wave_widget.dart'; import 'package:flutter_soloud_example/visualizer/bmp_header.dart'; import 'package:flutter_soloud_example/visualizer/paint_texture.dart'; -/// enum to tell [Visualizer] to build a texture as: -/// [both1D] frequencies data on the 1st 256px row, wave on the 2nd 256px -/// [fft2D] frequencies data 256x256 px -/// [wave2D] wave data 256x256px -/// [both2D] both frequencies & wave data interleaved 256x512px -enum TextureType { - both1D, - fft2D, - wave2D, - both2D, // no implemented yet -} - -class FftController extends ChangeNotifier { - FftController({ - this.minFreqRange = 0, - this.maxFreqRange = 255, +class VisualizerController extends ChangeNotifier { + VisualizerController({ this.isVisualizerEnabled = true, this.isVisualizerForPlayer = true, this.isCaptureStarted = false, - }); + this.samplesKind = GetSamplesKind.texture, + }) : maxRangeLimit = samplesKind == GetSamplesKind.texture ? 511 : 255, + maxRange = samplesKind == GetSamplesKind.texture ? 511 : 255, + minRange = 0 { + audioData = AudioData( + isVisualizerForPlayer ? GetSamplesFrom.player : GetSamplesFrom.microphone, + samplesKind, + ); + } - int minFreqRange; - int maxFreqRange; + int maxRangeLimit; + int minRange; + int maxRange; bool isVisualizerEnabled; bool isVisualizerForPlayer; bool isCaptureStarted; + GetSamplesKind samplesKind; + late AudioData audioData; - void changeMinFreq(int minFreq) { - if (minFreq < 0) return; - if (minFreq >= maxFreqRange) return; - minFreqRange = minFreq; - notifyListeners(); + void changeMin(int min, {bool notify = true}) { + minRange = min.clamp(0, maxRange); + if (notify) { + notifyListeners(); + } } - void changeMaxFreq(int maxFreq) { - if (maxFreq > 255) return; - if (maxFreq <= minFreqRange) return; - maxFreqRange = maxFreq; - notifyListeners(); + void changeMax(int max, {bool notify = true}) { + final nMax = samplesKind == GetSamplesKind.texture ? 511 : 255; + maxRange = max.clamp(minRange, nMax); + if (notify) { + notifyListeners(); + } } void changeIsVisualizerForPlayer(bool isForPlayer) { isVisualizerForPlayer = isForPlayer; + audioData.dispose(); + audioData = AudioData( + isVisualizerForPlayer ? GetSamplesFrom.player : GetSamplesFrom.microphone, + samplesKind, + ); notifyListeners(); } @@ -71,51 +72,58 @@ class FftController extends ChangeNotifier { isCaptureStarted = enabled; notifyListeners(); } + + void changeSamplesKind(GetSamplesKind kind) { + samplesKind = kind; + switch (kind) { + case GetSamplesKind.linear: + changeMin(0, notify: false); + changeMax(255, notify: false); + maxRangeLimit = 255; + case GetSamplesKind.texture: + changeMin(0, notify: false); + changeMax(511, notify: false); + maxRangeLimit = 511; + case GetSamplesKind.wave: + changeMin(0, notify: false); + changeMax(255, notify: false); + maxRangeLimit = 255; + } + audioData.changeType( + isVisualizerForPlayer ? GetSamplesFrom.player : GetSamplesFrom.microphone, + samplesKind, + ); + notifyListeners(); + } } class Visualizer extends StatefulWidget { const Visualizer({ required this.controller, required this.shader, - this.textureType = TextureType.fft2D, super.key, }); - final FftController controller; - final ui.FragmentShader shader; - final TextureType textureType; + final VisualizerController controller; + final String shader; @override State createState() => _VisualizerState(); } -class _VisualizerState extends State - with SingleTickerProviderStateMixin { +class _VisualizerState extends State with TickerProviderStateMixin { late Ticker ticker; late Stopwatch sw; - late Bmp32Header fftImageRow; - late Bmp32Header fftImageMatrix; - late int fftSize; - late int halfFftSize; - late int fftBitmapRange; - ffi.Pointer> playerData = ffi.nullptr; - ffi.Pointer> captureData = ffi.nullptr; + late Bmp32Header image; + late int bitmapRange; late Future Function() buildImageCallback; - late int Function(int row, int col) textureTypeCallback; + late int Function(SampleRow row, SampleColumn col) textureTypeCallback; int nFrames = 0; @override void initState() { super.initState(); - /// these constants must not be touched since SoLoud - /// gives back a size of 256 values - fftSize = 512; - halfFftSize = fftSize >> 1; - - playerData = calloc(); - captureData = calloc(); - ticker = createTicker(_tick); sw = Stopwatch(); sw.start(); @@ -126,7 +134,10 @@ class _VisualizerState extends State SoLoudCapture.instance.isCaptureStarted(); widget.controller.addListener(() { - ticker.stop(); + ticker + ..stop() + ..dispose(); + ticker = createTicker(_tick); setupBitmapSize(); ticker.start(); sw.reset(); @@ -136,55 +147,65 @@ class _VisualizerState extends State @override void dispose() { - ticker.stop(); + ticker + ..stop() + ..dispose(); sw.stop(); - calloc.free(playerData); - playerData = ffi.nullptr; - calloc.free(captureData); - captureData = ffi.nullptr; super.dispose(); } void _tick(Duration elapsed) { nFrames++; - if (mounted) { - setState(() {}); + if (context.mounted) { + try { + widget.controller.audioData.updateSamples(); + setState(() {}); + } on Exception catch (e) { + debugPrint('$e'); + } } } - void setupBitmapSize() { - fftBitmapRange = - widget.controller.maxFreqRange - widget.controller.minFreqRange; - fftImageRow = Bmp32Header.setHeader(fftBitmapRange, 2); - fftImageMatrix = Bmp32Header.setHeader(fftBitmapRange, 256); - - switch (widget.textureType) { - case TextureType.both1D: - { - buildImageCallback = buildImageFromLatestSamplesRow; - break; - } - case TextureType.fft2D: - { - buildImageCallback = buildImageFromAllSamplesMatrix; - textureTypeCallback = getFFTDataCallback; - break; - } - case TextureType.wave2D: - { - buildImageCallback = buildImageFromAllSamplesMatrix; - textureTypeCallback = getWaveDataCallback; - break; - } - // TODO(marco): implement this - case TextureType.both2D: - { - buildImageCallback = buildImageFromAllSamplesMatrix; - textureTypeCallback = getWaveDataCallback; - break; - } - } - } + // @override + // Widget build(BuildContext context) { + // return Row( + // children: [ + // Column( + // children: [ + // const Text( + // 'FFT data', + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + + // /// FFT bars + // BarsFftWidget( + // audioData: widget.controller.audioData, + // minFreq: widget.controller.minRange, + // maxFreq: widget.controller.maxRange, + // width: 250, + // height: 120, + // ), + // ], + // ), + // const SizedBox(width: 6), + // Column( + // children: [ + // const Text( + // '256 wave data', + // style: TextStyle(fontWeight: FontWeight.bold), + // ), + + // /// wave data bars + // BarsWaveWidget( + // audioData: widget.controller.audioData, + // width: 250, + // height: 120, + // ), + // ], + // ), + // ], + // ); + // } @override Widget build(BuildContext context) { @@ -193,19 +214,13 @@ class _VisualizerState extends State builder: (context, dataTexture) { final fps = nFrames.toDouble() / (sw.elapsedMilliseconds / 1000.0); if (!dataTexture.hasData || dataTexture.data == null) { - return Placeholder( - color: Colors.yellow, - fallbackWidth: 100, - fallbackHeight: 100, + return const Placeholder( + color: Colors.red, strokeWidth: 0.5, - child: Text("can't get audio samples\n" - 'FPS: ${fps.toStringAsFixed(1)}'), + child: Text("\n can't get audio samples \n"), ); } - final nFft = - widget.controller.maxFreqRange - widget.controller.minFreqRange; - return LayoutBuilder( builder: (context, constraints) { return Column( @@ -244,12 +259,28 @@ class _VisualizerState extends State sw.reset(); nFrames = 0; }, - child: AudioShader( - width: constraints.maxWidth, - height: constraints.maxWidth / 2.4, - image: dataTexture.data!, - shader: widget.shader, - iTime: sw.elapsedMilliseconds / 1000.0, + child: FutureBuilder( + future: loadShader(), + builder: (context, snapshot) { + if (snapshot.hasData) { + return AudioShader( + width: constraints.maxWidth, + height: constraints.maxWidth / 2.4, + image: dataTexture.data!, + shader: snapshot.data!, + iTime: sw.elapsedMilliseconds / 1000.0, + ); + } else { + if (snapshot.data == null) { + return const Placeholder( + child: Align( + child: Text('Error compiling shader.\nSee log'), + ), + ); + } + return const CircularProgressIndicator(); + } + }, ), ), @@ -257,9 +288,9 @@ class _VisualizerState extends State children: [ Column( children: [ - Text( - '$nFft FFT data', - style: const TextStyle(fontWeight: FontWeight.bold), + const Text( + 'FFT data', + style: TextStyle(fontWeight: FontWeight.bold), ), /// FFT bars @@ -271,11 +302,9 @@ class _VisualizerState extends State nFrames = 0; }, child: BarsFftWidget( - audioData: widget.controller.isVisualizerForPlayer - ? playerData.value - : captureData.value, - minFreq: widget.controller.minFreqRange, - maxFreq: widget.controller.maxFreqRange, + audioData: widget.controller.audioData, + minFreq: widget.controller.minRange, + maxFreq: widget.controller.maxRange, width: constraints.maxWidth / 2 - 3, height: constraints.maxWidth / 6, ), @@ -299,9 +328,7 @@ class _VisualizerState extends State nFrames = 0; }, child: BarsWaveWidget( - audioData: widget.controller.isVisualizerForPlayer - ? playerData.value - : captureData.value, + audioData: widget.controller.audioData, width: constraints.maxWidth / 2 - 3, height: constraints.maxWidth / 6, ), @@ -318,142 +345,183 @@ class _VisualizerState extends State ); } - /// build an image to be passed to the shader. - /// The image is a matrix of 256x2 RGBA pixels representing: - /// in the 1st row the frequencies data - /// in the 2nd row the wave data - Future buildImageFromLatestSamplesRow() async { - if (!widget.controller.isVisualizerEnabled) { - return null; + /// load asynchronously the fragment shader + Future loadShader() async { + try { + final program = await ui.FragmentProgram.fromAsset(widget.shader); + return program.fragmentShader(); + } catch (e) { + debugPrint('error compiling the shader $e'); } + return null; + } - /// get audio data from player or capture device - if (widget.controller.isVisualizerForPlayer) { - try { - SoLoud.instance.getAudioTexture2D(playerData); - } catch (e) { - return null; - } - } else if (!widget.controller.isVisualizerForPlayer && - widget.controller.isCaptureStarted) { - final ret = SoLoudCapture.instance.getCaptureAudioTexture2D(captureData); - if (ret != CaptureErrors.captureNoError) { - return null; - } - } else { - return null; + void setupBitmapSize() { + bitmapRange = widget.controller.maxRange - widget.controller.minRange + 1; + + switch (widget.controller.samplesKind) { + case GetSamplesKind.wave: + { + image = Bmp32Header.setHeader(bitmapRange, 1); + buildImageCallback = buildImageForWave; + break; + } + case GetSamplesKind.linear: + { + image = Bmp32Header.setHeader(bitmapRange, 2); + buildImageCallback = buildImageForLinear; + break; + } + case GetSamplesKind.texture: + { + image = Bmp32Header.setHeader(bitmapRange, 256); + buildImageCallback = buildImageForTexture; + break; + } } + } - if (!mounted) { + /// Build an image to be passed to the shader. + /// The image is a matrix of 256x1 RGBA pixels representing the wave data. + Future buildImageForWave() async { + if (!context.mounted) { + return null; + } + if (!(widget.controller.isVisualizerEnabled && + SoLoud.instance.getVoiceCount() > 0) && + !widget.controller.isCaptureStarted) { return null; } final completer = Completer(); - final bytes = Uint8List(fftBitmapRange * 2 * 4); + final bytes = Uint8List(bitmapRange * 4); // Fill the texture bitmap var col = 0; - for (var i = widget.controller.minFreqRange; - i < widget.controller.maxFreqRange; + for (var i = widget.controller.minRange; + i <= widget.controller.maxRange; ++i, ++col) { - // fill 1st bitmap row with magnitude - bytes[col * 4 + 0] = getFFTDataCallback(0, i); + // fill bitmap row with wave data + final z = getWave(SampleWave(i)); + bytes[col * 4 + 0] = z; bytes[col * 4 + 1] = 0; bytes[col * 4 + 2] = 0; bytes[col * 4 + 3] = 255; - // fill 2nd bitmap row with amplitude - bytes[(fftBitmapRange + col) * 4 + 0] = getWaveDataCallback(0, i); - bytes[(fftBitmapRange + col) * 4 + 1] = 0; - bytes[(fftBitmapRange + col) * 4 + 2] = 0; - bytes[(fftBitmapRange + col) * 4 + 3] = 255; } - final img = fftImageRow.storeBitmap(bytes); + final img = image.storeBitmap(bytes); ui.decodeImageFromList(img, completer.complete); return completer.future; } - /// build an image to be passed to the shader. - /// The image is a matrix of 256x256 RGBA pixels representing - /// rows of wave data or frequencies data. - /// Passing [getWaveDataCallback] as parameter, it will return wave data - /// Passing [getFFTDataCallback] as parameter, it will return FFT data - Future buildImageFromAllSamplesMatrix() async { - if (!widget.controller.isVisualizerEnabled) { + /// Build an image to be passed to the shader. + /// The image is a matrix of 256x2 RGBA pixels representing: + /// in the 1st row the frequencies data + /// in the 2nd row the wave data + Future buildImageForLinear() async { + if (!context.mounted) { return null; } - - /// get audio data from player or capture device - if (widget.controller.isVisualizerForPlayer) { - try { - SoLoud.instance.getAudioTexture2D(playerData); - } catch (e) { - return null; - } - } else if (!widget.controller.isVisualizerForPlayer && - widget.controller.isCaptureStarted) { - final ret = SoLoudCapture.instance.getCaptureAudioTexture2D(captureData); - if (ret != CaptureErrors.captureNoError) { - return null; - } - } else { + if (!(widget.controller.isVisualizerEnabled && + SoLoud.instance.getVoiceCount() > 0) && + !widget.controller.isCaptureStarted) { return null; } - if (!mounted) { - return null; + final completer = Completer(); + final bytes = Uint8List(bitmapRange * 4 * 2); + var col = 0; + // Fill the texture bitmap + for (var i = widget.controller.minRange; + i <= widget.controller.maxRange; + ++i, ++col) { + // fill 1st bitmap row with FFT magnitude + bytes[col * 4 + 0] = getLinearFft(SampleLinear(i)); + bytes[col * 4 + 1] = 0; + bytes[col * 4 + 2] = 0; + bytes[col * 4 + 3] = 255; + // fill 2nd bitmap row with wave amplitudes + bytes[col * 4 + 256 * 4 + 0] = getLinearWave(SampleLinear(i)); + bytes[col * 4 + 256 * 4 + 1] = 0; + bytes[col * 4 + 256 * 4 + 2] = 0; + bytes[col * 4 + 256 * 4 + 3] = 255; } - /// IMPORTANT: if [mounted] is not checked here, could happens that - /// dispose() is called before this is called but it is called! - /// Since in dispose the [audioData] is freed, there will be a crash! - /// I do not understand why this happens because the FutureBuilder - /// seems has not finished before dispose()!? - if (!mounted) { + final img = image.storeBitmap(bytes); + ui.decodeImageFromList(img, completer.complete); + + return completer.future; + } + + /// Build an image to be passed to the shader. + /// The image is a matrix of 256x256 RGBA pixels representing + /// rows of wave data or frequencies data. + Future buildImageForTexture() async { + if (!context.mounted) { return null; } + if (!(widget.controller.isVisualizerEnabled && + SoLoud.instance.getVoiceCount() > 0) && + !widget.controller.isCaptureStarted) { + return null; + } + + final width = widget.controller.maxRange - widget.controller.minRange; + + /// On the web there are worst performance getting data because for every + /// single data a JS function must be called. + /// Setting here an height of 100 instead of 256 to improve. + const height = kIsWeb ? 100 : 256; + final completer = Completer(); - final bytes = Uint8List(fftBitmapRange * 256 * 4); + final bytes = Uint8List(width * height * 4); // Fill the texture bitmap with wave data - for (var y = 0; y < 256; ++y) { + var row = 0; + for (var y = 0; y < height; ++y, ++row) { var col = 0; - for (var x = widget.controller.minFreqRange; - x < widget.controller.maxFreqRange; - ++x, ++col) { - bytes[y * fftBitmapRange * 4 + col * 4 + 0] = textureTypeCallback(y, x); - bytes[y * fftBitmapRange * 4 + col * 4 + 1] = 0; - bytes[y * fftBitmapRange * 4 + col * 4 + 2] = 0; - bytes[y * fftBitmapRange * 4 + col * 4 + 3] = 255; + for (var x = 0; x < width; ++x, ++col) { + final z = getTexture(SampleRow(y), SampleColumn(x)); + bytes[row * width * 4 + col * 4 + 0] = z; + bytes[row * width * 4 + col * 4 + 1] = 0; + bytes[row * width * 4 + col * 4 + 2] = 0; + bytes[row * width * 4 + col * 4 + 3] = 255; } } - final img = fftImageMatrix.storeBitmap(bytes); + image = Bmp32Header.setHeader(width, height); + final img = image.storeBitmap(bytes); ui.decodeImageFromList(img, completer.complete); + // final ui.Codec codec = await ui.instantiateImageCodec(img); + // final ui.FrameInfo frameInfo = await codec.getNextFrame(); + // completer.complete(frameInfo.image); return completer.future; } - int getFFTDataCallback(int row, int col) { - if (widget.controller.isVisualizerForPlayer) { - return (playerData.value[row * fftSize + col] * 255.0).toInt(); - } else { - return (captureData.value[row * fftSize + col] * 255.0).toInt(); - } + int getWave(SampleWave offset) { + final n = widget.controller.audioData.getWave(offset); + return (((n + 1.0) / 2.0).clamp(0, 1) * 128).toInt(); } - int getWaveDataCallback(int row, int col) { - if (widget.controller.isVisualizerForPlayer) { - return (((playerData.value[row * fftSize + halfFftSize + col] + 1.0) / - 2.0) * - 128) - .toInt(); - } else { - return (((captureData.value[row * fftSize + halfFftSize + col] + 1.0) / - 2.0) * - 128) - .toInt(); - } + int getLinearFft(SampleLinear offset) { + return (widget.controller.audioData.getLinearFft(offset).clamp(0, 1) * 255) + .toInt(); + } + + int getLinearWave(SampleLinear offset) { + final n = widget.controller.audioData.getLinearWave(offset).abs(); + return (((n + 1.0) / 2.0).clamp(0, 1) * 128).toInt(); + } + + int getTexture(SampleRow row, SampleColumn col) { + final n = widget.controller.audioData.getTexture(row, col); + + /// With col<256 we are asking for FFT values. + if (col.value < 256) return (n.clamp(0, 1) * 255).toInt(); + + /// With col>256 we are asking for wave values. + return (((n + 1.0) / 2.0).clamp(0, 1) * 128).toInt(); } } diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..092d222 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} diff --git a/example/tests/tests.dart b/example/tests/tests.dart index afae10a..5688e73 100644 --- a/example/tests/tests.dart +++ b/example/tests/tests.dart @@ -1,100 +1,258 @@ import 'dart:async'; -import 'dart:io'; +import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_soloud/flutter_soloud.dart'; -import 'package:flutter_soloud/src/soloud_controller.dart'; -import 'package:logging/logging.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.dart'; -/// An end-to-end test. +enum TestStatus { + none, + passed, + failed, +} + +typedef TestFunction = ({ + String name, + Future Function() callback, + TestStatus status, +}); + +/// A GUI for tests. /// /// Run this with `flutter run tests/tests.dart`. -void main() async { - // Make sure we can see logs from the engine, even in release mode. - // ignore: avoid_print - final errorsBuffer = StringBuffer(); - Logger.root.onRecord.listen((record) { - debugPrint(record.toString(), wrapWidth: 80); - if (record.level >= Level.WARNING) { - // Exception for deiniting. - if (record.error is SoLoudInitializationStoppedByDeinitException) { - return; - } - - // Make sure the warnings are visible. - stderr.writeln('TEST error (${record.level} log): $record'); - errorsBuffer.writeln('- $record'); - // Set exit code but keep running to see all logs. - exitCode = 1; - } - }); - Logger.root.level = Level.ALL; - +void main() { WidgetsFlutterBinding.ensureInitialized(); - var tests = Function()>[ - testProtectVoice, - testAllInstancesFinished, - testCreateNotes, - testPlaySeekPause, - testPan, - testHandles, - loopingTests, - ]; - for (final f in tests) { - await runZonedGuarded( - () async => f(), - (error, stack) => printError, + runApp( + MaterialApp( + themeMode: ThemeMode.dark, + darkTheme: ThemeData.dark(useMaterial3: true), + scrollBehavior: const MaterialScrollBehavior().copyWith( + // enable mouse dragging + dragDevices: PointerDeviceKind.values.toSet(), + ), + home: const Padding( + padding: EdgeInsets.all(8), + child: MyHomePage(), + ), + ), + ); +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key}); + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final output = StringBuffer(); + final List tests = []; + final textEditingController = TextEditingController(); + + @override + void initState() { + super.initState(); + + /// Add all testing functions. + tests.addAll([ + ( + name: 'testProtectVoice', + status: TestStatus.none, + callback: testProtectVoice, + ), + ( + name: 'testAllInstancesFinished', + status: TestStatus.none, + callback: testAllInstancesFinished, + ), + ( + name: 'testCreateNotes', + status: TestStatus.none, + callback: testCreateNotes, + ), + ( + name: 'testPlaySeekPause', + status: TestStatus.none, + callback: testPlaySeekPause, + ), + ( + name: 'testPan', + status: TestStatus.none, + callback: testPan, + ), + ( + name: 'testHandles', + status: TestStatus.none, + callback: testHandles, + ), + ( + name: 'loopingTests', + status: TestStatus.none, + callback: loopingTests, + ), + ( + name: 'testSynchronousDeinit', + status: TestStatus.none, + callback: testSynchronousDeinit, + ), + ( + name: 'testAsynchronousDeinit', + status: TestStatus.none, + callback: testAsynchronousDeinit, + ), + ]); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return SafeArea( + child: Scaffold( + body: Column( + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + OutlinedButton( + onPressed: () async { + for (var i = 0; i < tests.length; i++) { + await runTest(i); + } + }, + child: const Text('Run All'), + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate( + tests.length, + (index) { + return OutlinedButton( + style: ButtonStyle( + backgroundColor: tests[index].status == + TestStatus.failed + ? const WidgetStatePropertyAll(Colors.red) + : tests[index].status == TestStatus.passed + ? const WidgetStatePropertyAll(Colors.green) + : null, + ), + onPressed: () async { + await runTest(index); + }, + child: Text( + tests[index].name, + ), + ); + }, + ), + ), + const SizedBox(height: 16), + ], + ), + Expanded( + child: TextField( + controller: textEditingController, + style: const TextStyle(color: Colors.black, fontSize: 12), + expands: true, + maxLines: null, + decoration: const InputDecoration( + fillColor: Colors.white, + filled: true, + ), + ), + ), + ], + ), + ), ); } - tests = Function()>[ - testSynchronousDeinit, - testAsynchronousDeinit, - ]; - for (final f in tests) { - await runZonedGuarded( - () async => f(), + /// Run text with index [index]. + /// + /// This outputs the asserts logs and the `StringBuffer` returned by + /// the test functions. + /// It also update the state of text buttons. + Future runTest(int index) async { + await runZonedGuarded>( + () async { + output + ..write('===== RUNNING "${tests[index].name}" =====\n') + ..write(await tests[index].callback()) + ..write('===== PASSED! =====\n\n') + ..writeln(); + tests[index] = ( + name: tests[index].name, + status: TestStatus.passed, + callback: tests[index].callback, + ); + textEditingController.text = output.toString(); + debugPrint(output.toString()); + if (context.mounted) setState(() {}); + }, (error, stack) { - if (error is SoLoudInitializationStoppedByDeinitException) { - // This is to be expected in this test. - return; - } - printError(error, stack); + // if (error is SoLoudInitializationStoppedByDeinitException) { + // // This is to be expected in this test. + // return; + // } + output + ..write('== TESTS "${tests[index].name}" FAILED with ' + 'the following error(s) ==') + ..writeln() + ..writeAll([error, stack], '\n\n') + ..writeln() + ..writeln(); + // ignore: parameter_assignments + tests[index] = ( + name: tests[index].name, + status: TestStatus.failed, + callback: tests[index].callback, + ); + textEditingController.text = output.toString(); + debugPrint(output.toString()); + if (context.mounted) setState(() {}); }, ); } +} - stdout.write('\n\n\n---\n\n\n'); - - if (exitCode != 0) { - // Since we're running this inside `flutter run`, the exit code - // will be overridden to 0 by the Flutter tool. - // The following is making sure that the errors are noticed. - stderr - ..writeln('===== TESTS FAILED with the following error(s) =====') - ..writeln() - ..writeln(errorsBuffer.toString()) - ..writeln() - ..writeln('See logs above for details.') - ..writeln(); - } else { - debugPrint('===== TESTS PASSED! ====='); - stdout - ..writeln('===== TESTS PASSED! =====') - ..writeln(); - } +// //////////////////////////// +// / Common methods +// //////////////////////////// + +Future initialize() async { + await SoLoud.instance.init(); + SoLoud.instance.setGlobalVolume(0.2); +} - // Cleanly close the app. - await SystemChannels.platform.invokeMethod('SystemNavigator.pop'); +void deinit() { + SoLoud.instance.deinit(); } -String output = ''; -AudioSource? currentSound; +Future delay(int ms) async { + await Future.delayed(Duration(milliseconds: ms), () {}); +} + +bool closeTo(num value, num expected, num epsilon) { + return (value - expected).abs() <= epsilon.abs(); +} + +Future loadAsset() async { + return SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); +} + +// /////////////////////////// +// / Tests +// /////////////////////////// /// Test setMaxActiveVoiceCount, setProtectedVoice and getProtectedVoice -Future testProtectVoice() async { +Future testProtectVoice() async { await initialize(); final defaultVoiceCount = SoLoud.instance.getMaxActiveVoiceCount(); @@ -147,11 +305,13 @@ Future testProtectVoice() async { 'Max active voices are not reset to the default value after reinit!', ); deinit(); + + return StringBuffer(); } /// Test allInstancesFinished stream -Future testAllInstancesFinished() async { - final log = Logger('testAllInstancesFinished'); +Future testAllInstancesFinished() async { + final ret = StringBuffer(); await initialize(); await SoLoud.instance.disposeAllSources(); @@ -170,14 +330,14 @@ Future testAllInstancesFinished() async { var songDisposed = false; unawaited( explosion.allInstancesFinished.first.then((_) async { - log.info('All instances of explosion finished.'); + ret.write('All instances of explosion finished.\n'); await SoLoud.instance.disposeSource(explosion); explosionDisposed = true; }), ); unawaited( song.allInstancesFinished.first.then((_) async { - log.info('All instances of song finished.'); + ret.write('All instances of song finished.\n'); await SoLoud.instance.disposeSource(song); songDisposed = true; }), @@ -198,127 +358,12 @@ Future testAllInstancesFinished() async { assert(songDisposed, "Song sound wasn't disposed."); deinit(); -} - -/// Test asynchronous `init()`-`deinit()` -Future testAsynchronousDeinit() async { - final log = Logger('testAsynchronousDeinit'); - - /// test asynchronous init-deinit looping with a short decreasing time - for (var t = 100; t >= 0; t--) { - var error = ''; - - /// Initialize the player - unawaited( - SoLoud.instance.init().then( - (_) {}, - onError: (Object e) { - if (e is SoLoudInitializationStoppedByDeinitException) { - // This is to be expected. - log.info('$e'); - return; - } - e = 'TEST FAILED delay: $t. Player starting error: $e'; - error = e.toString(); - }, - ), - ); - assert(error.isEmpty, error); - - /// wait for [t] ms and deinit() - await delay(t); - SoLoud.instance.deinit(); - final after = SoLoudController().soLoudFFI.isInited(); - - assert( - after == false, - 'TEST FAILED delay: $t. The player has not been deinited correctly!', - ); - - stderr.writeln('------------- awaited init delay $t passed\n'); - } -} - -/// Test synchronous `init()`-`deinit()` -Future testSynchronousDeinit() async { - final log = Logger('testSynchronousDeinit'); - - /// test synchronous init-deinit looping with a short decreasing time - /// waiting for `initialize()` to finish - for (var t = 100; t >= 0; t--) { - var error = ''; - - /// Initialize the player - await SoLoud.instance.init().then( - (_) {}, - onError: (Object e) { - if (e is SoLoudInitializationStoppedByDeinitException) { - // This is to be expected. - log.info('$e'); - return; - } - e = 'TEST FAILED delay: $t. Player starting error: $e'; - error = e.toString(); - }, - ); - assert( - error.isEmpty, - 'ASSERT FAILED delay: $t. The player has not been ' - 'inited correctly!', - ); - - SoLoud.instance.deinit(); - - assert( - !SoLoud.instance.isInitialized || - !SoLoudController().soLoudFFI.isInited(), - 'ASSERT FAILED delay: $t. The player has not been ' - 'inited or deinited correctly!', - ); - - stderr.writeln('------------- awaited init #$t passed\n'); - } - - /// Try init-play-deinit and again init-play without disposing the sound - await SoLoud.instance.init(); - SoLoud.instance.setGlobalVolume(0.2); - - await loadAsset(); - await SoLoud.instance.play(currentSound!); - await delay(100); - await SoLoud.instance.play(currentSound!); - await delay(100); - await SoLoud.instance.play(currentSound!); - - await delay(2000); - - SoLoud.instance.deinit(); - - /// Initialize again and check if the sound has been - /// disposed correctly by `deinit()` - await SoLoud.instance.init(); - assert( - SoLoudController() - .soLoudFFI - .getIsValidVoiceHandle(currentSound!.handles.first) == - false, - 'getIsValidVoiceHandle(): sound not disposed by the engine', - ); - assert( - SoLoudController().soLoudFFI.countAudioSource(currentSound!.soundHash) == 0, - 'getCountAudioSource(): sound not disposed by the engine', - ); - assert( - SoLoudController().soLoudFFI.getActiveVoiceCount() == 0, - 'getActiveVoiceCount(): sound not disposed by the engine', - ); - SoLoud.instance.deinit(); + return ret; } /// Test waveform -/// -Future testCreateNotes() async { +Future testCreateNotes() async { await initialize(); final notes0 = await SoLoudTools.createNotes( @@ -332,7 +377,7 @@ Future testCreateNotes() async { ); assert( notes0.length == 12 && notes1.length == 12 && notes2.length == 12, - 'SoLoudTools.createNotes() failed!', + 'SoLoudTools.createNotes() failed!\n', ); await SoLoud.instance.play(notes1[5]); @@ -366,42 +411,46 @@ Future testCreateNotes() async { await SoLoud.instance.stop(notes1[1].handles.first); deinit(); + + return StringBuffer(); } /// Test play, pause, seek, position /// -Future testPlaySeekPause() async { +Future testPlaySeekPause() async { /// Start audio isolate await initialize(); /// Load sample - await loadAsset(); + final currentSound = + await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); /// pause, seek test { - await SoLoud.instance.play(currentSound!); - final length = SoLoud.instance.getLength(currentSound!); + await SoLoud.instance.play(currentSound); + final length = SoLoud.instance.getLength(currentSound); assert( length.inMilliseconds == 3840, - 'getLength() failed: ${length.inMilliseconds}!', + 'getLength() failed: ${length.inMilliseconds}!\n', ); await delay(1000); - SoLoud.instance.pauseSwitch(currentSound!.handles.first); - final paused = SoLoud.instance.getPause(currentSound!.handles.first); + SoLoud.instance.pauseSwitch(currentSound.handles.first); + final paused = SoLoud.instance.getPause(currentSound.handles.first); assert(paused, 'pauseSwitch() failed!'); /// seek const wantedPosition = Duration(seconds: 2); - SoLoud.instance.seek(currentSound!.handles.first, wantedPosition); - final position = SoLoud.instance.getPosition(currentSound!.handles.first); + SoLoud.instance.seek(currentSound.handles.first, wantedPosition); + final position = SoLoud.instance.getPosition(currentSound.handles.first); assert(position == wantedPosition, 'getPosition() failed!'); } deinit(); + return StringBuffer(); } /// Test instancing playing handles and their disposal -Future testPan() async { +Future testPan() async { /// Start audio isolate await initialize(); @@ -422,20 +471,33 @@ Future testPan() async { await delay(1000); deinit(); + return StringBuffer(); } /// Test instancing playing handles and their disposal -Future testHandles() async { +Future testHandles() async { + var output = ''; + /// Start audio isolate await initialize(); /// Load sample - await loadAsset(); + final currentSound = + await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); + + currentSound.soundEvents.listen((event) { + if (event.event == SoundEventType.handleIsNoMoreValid) { + output = 'SoundEvent.handleIsNoMoreValid'; + } + if (event.event == SoundEventType.soundDisposed) { + output = 'SoundEvent.soundDisposed'; + } + }); /// Play sample - await SoLoud.instance.play(currentSound!); + await SoLoud.instance.play(currentSound); assert( - currentSound!.soundHash.isValid && currentSound!.handles.length == 1, + currentSound.soundHash.isValid && currentSound.handles.length == 1, 'play() failed!', ); @@ -447,12 +509,12 @@ Future testHandles() async { ); /// Play 4 sample - await SoLoud.instance.play(currentSound!); - await SoLoud.instance.play(currentSound!); - await SoLoud.instance.play(currentSound!); - await SoLoud.instance.play(currentSound!); + await SoLoud.instance.play(currentSound); + await SoLoud.instance.play(currentSound); + await SoLoud.instance.play(currentSound); + await SoLoud.instance.play(currentSound); assert( - currentSound!.handles.length == 4, + currentSound.handles.length == 4, 'loadFromAssets() failed!', ); @@ -461,77 +523,156 @@ Future testHandles() async { /// 3798ms explosion.mp3 sample duration await delay(4500); assert( - currentSound!.handles.isEmpty, + currentSound.handles.isEmpty, 'Play 4 sample handles failed!', ); deinit(); + return StringBuffer(); } /// Test looping state and `loopingStartAt` -Future loopingTests() async { +Future loopingTests() async { await initialize(); - await loadAsset(); + /// Load sample + final currentSound = + await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); await SoLoud.instance.play( - currentSound!, + currentSound, looping: true, loopingStartAt: const Duration(seconds: 1), ); assert( - SoLoud.instance.getLooping(currentSound!.handles.first), + SoLoud.instance.getLooping(currentSound.handles.first), 'looping failed!', ); /// Wait for the first loop to start at 1s await delay(4100); assert( - SoLoud.instance.getLoopPoint(currentSound!.handles.first) == + SoLoud.instance.getLoopPoint(currentSound.handles.first) == const Duration(seconds: 1) && - SoLoud.instance.getPosition(currentSound!.handles.first) > + SoLoud.instance.getPosition(currentSound.handles.first) > const Duration(seconds: 1), 'looping start failed!', ); deinit(); + return StringBuffer(); } -/// Common methods -Future initialize() async { - await SoLoud.instance.init(); - SoLoud.instance.setGlobalVolume(0.2); -} +/// Test asynchronous `init()`-`deinit()` +Future testAsynchronousDeinit() async { + /// test asynchronous init-deinit looping with a short decreasing time + for (var t = 100; t >= 0; t--) { + var error = ''; -void deinit() { - SoLoud.instance.deinit(); -} + /// Initialize the player + unawaited( + SoLoud.instance.init().then( + (_) {}, + onError: (Object e) { + if (e is SoLoudInitializationStoppedByDeinitException) { + // This is to be expected. + debugPrint('$e\n'); + return; + } + debugPrint('TEST FAILED delay: $t. Player starting error: $e\n'); + error = e.toString(); + }, + ), + ); -Future delay(int ms) async { - await Future.delayed(Duration(milliseconds: ms), () {}); + assert(error.isEmpty, error); + + /// wait for [t] ms and deinit() + await delay(t); + SoLoud.instance.deinit(); + final after = SoLoudController().soLoudFFI.isInited(); + + assert( + after == false, + 'TEST FAILED delay: $t. The player has not been deinited correctly!', + ); + + debugPrint('------------- awaited init delay $t passed\n'); + } + return StringBuffer(); } -Future loadAsset() async { - if (currentSound != null) { - await SoLoud.instance.disposeSource(currentSound!); +/// Test synchronous `init()`-`deinit()` +Future testSynchronousDeinit() async { + /// test synchronous init-deinit looping with a short decreasing time + /// waiting for `initialize()` to finish + for (var t = 100; t >= 0; t--) { + var error = ''; + + /// Initialize the player + await SoLoud.instance.init().then( + (_) {}, + onError: (Object e) { + if (e is SoLoudInitializationStoppedByDeinitException) { + // This is to be expected. + debugPrint('$e\n'); + return; + } + debugPrint('TEST FAILED delay: $t. Player starting error: $e'); + error = e.toString(); + }, + ); + assert(error.isEmpty, error); + + SoLoud.instance.deinit(); + + assert( + !SoLoud.instance.isInitialized || + !SoLoudController().soLoudFFI.isInited(), + 'ASSERT FAILED delay: $t. The player has not been ' + 'inited or deinited correctly!', + ); + + debugPrint('------------- awaited init #$t passed\n'); } - currentSound = await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); - currentSound!.soundEvents.listen((event) { - if (event.event == SoundEventType.handleIsNoMoreValid) { - output = 'SoundEvent.handleIsNoMoreValid'; - } - if (event.event == SoundEventType.soundDisposed) { - output = 'SoundEvent.soundDisposed'; - } - }); -} + /// Try init-play-deinit and again init-play without disposing the sound + await SoLoud.instance.init(); + SoLoud.instance.setGlobalVolume(0.2); -bool closeTo(num value, num expected, num epsilon) { - return (value - expected).abs() <= epsilon.abs(); -} + /// Load sample + final currentSound = + await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); + + await SoLoud.instance.play(currentSound); + await delay(100); + await SoLoud.instance.play(currentSound); + await delay(100); + await SoLoud.instance.play(currentSound); + + await delay(2000); + + SoLoud.instance.deinit(); + + /// Initialize again and check if the sound has been + /// disposed correctly by `deinit()` + await SoLoud.instance.init(); + assert( + SoLoudController() + .soLoudFFI + .getIsValidVoiceHandle(currentSound.handles.first) == + false, + 'getIsValidVoiceHandle(): sound not disposed by the engine', + ); + assert( + SoLoudController().soLoudFFI.countAudioSource(currentSound.soundHash) == 0, + 'getCountAudioSource(): sound not disposed by the engine', + ); + assert( + SoLoudController().soLoudFFI.getActiveVoiceCount() == 0, + 'getActiveVoiceCount(): sound not disposed by the engine', + ); + SoLoud.instance.deinit(); -void printError(Object error, StackTrace stack) { - stderr.writeln('TEST error: $error\nstack: $stack'); - exitCode = 1; + return StringBuffer(); } diff --git a/example/web/favicon.png b/example/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/example/web/favicon.png differ diff --git a/example/web/icons/Icon-192.png b/example/web/icons/Icon-192.png new file mode 100644 index 0000000..b749bfe Binary files /dev/null and b/example/web/icons/Icon-192.png differ diff --git a/example/web/icons/Icon-512.png b/example/web/icons/Icon-512.png new file mode 100644 index 0000000..88cfd48 Binary files /dev/null and b/example/web/icons/Icon-512.png differ diff --git a/example/web/icons/Icon-maskable-192.png b/example/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/example/web/icons/Icon-maskable-192.png differ diff --git a/example/web/icons/Icon-maskable-512.png b/example/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/example/web/icons/Icon-maskable-512.png differ diff --git a/example/web/index.html b/example/web/index.html new file mode 100644 index 0000000..94a97ee --- /dev/null +++ b/example/web/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + example + + + + + + + diff --git a/example/web/manifest.json b/example/web/manifest.json new file mode 100644 index 0000000..096edf8 --- /dev/null +++ b/example/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/img/audacity_spectrum.png b/img/audacity_spectrum.png deleted file mode 100644 index 0d2cffe..0000000 Binary files a/img/audacity_spectrum.png and /dev/null differ diff --git a/img/flutter_soloud_spectrum.png b/img/flutter_soloud_spectrum.png deleted file mode 100644 index 650467d..0000000 Binary files a/img/flutter_soloud_spectrum.png and /dev/null differ diff --git a/img/wasmWorker.png b/img/wasmWorker.png new file mode 100644 index 0000000..23ea479 Binary files /dev/null and b/img/wasmWorker.png differ diff --git a/lib/flutter_soloud.dart b/lib/flutter_soloud.dart index ddc7ec9..a860ad2 100644 --- a/lib/flutter_soloud.dart +++ b/lib/flutter_soloud.dart @@ -2,6 +2,8 @@ library flutter_soloud; export 'src/audio_source.dart'; +export 'src/bindings/audio_data.dart'; +export 'src/bindings/audio_data_extensions.dart'; export 'src/enums.dart' hide PlayerErrors, PlayerStateNotification; export 'src/exceptions/exceptions.dart'; export 'src/filter_params.dart'; diff --git a/lib/src/bindings/audio_data.dart b/lib/src/bindings/audio_data.dart new file mode 100644 index 0000000..c581863 --- /dev/null +++ b/lib/src/bindings/audio_data.dart @@ -0,0 +1,262 @@ +import 'package:flutter_soloud/src/bindings/audio_data_extensions.dart'; +import 'package:flutter_soloud/src/bindings/audio_data_ffi.dart' + if (dart.library.js_interop) 'audio_data_web.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.dart'; +import 'package:flutter_soloud/src/exceptions/exceptions.dart'; +import 'package:meta/meta.dart'; + +/// Enum to tell [AudioData] from where to get audio data. +/// Every time [AudioData.updateSamples] is called, the audio data will +/// be acquired by the respective device. +enum GetSamplesFrom { + /// Take data from the player. + player, + + /// Take data from the microphone. + microphone, +} + +/// The way the audio data should be acquired. +/// +/// Every time [AudioData.updateSamples] is called it is possible to query the +/// acquired new audio data using [AudioData.getLinearFft], +/// [AudioData.getLinearWave], [AudioData.getTexture] or [AudioData.getWave]. +enum GetSamplesKind { + /// Get data in a linear manner: the first 256 floats are audio FFI values, + /// the other 256 are audio wave samples. + /// To get the audio data use [AudioData.getLinearFft] or + /// [AudioData.getLinearWave]. + linear, + + /// Get data in a 2D way. The resulting data will be a matrix of 256 + /// [linear] rows. Each time the [AudioData.updateSamples] method is called, + /// the last row is discarded and the new one will be the first. + /// To get the audio data use [AudioData.getTexture]. + texture, + + /// Get the 256 float of wave audio data. + /// To get the audio data use [AudioData.getWave]. + wave, +} + +/// Class to manage audio samples. +/// +/// The `visualization` must be enabled to be able to acquire data from the +/// player. You can achieve this by calling +/// `SoLoud.instance.setVisualizationEnabled(true);`. +/// +/// Audio samples can be get from the player or from the microphone, and +/// in a texture matrix or a linear array way. +/// +/// IMPORTANT: remember to call [dispose] method when there is no more need +/// to acquire audio. +/// +/// After calling [updateSamples] it's possible to call the proper getter +/// to have back the audio samples. For example, using a "Ticker" +/// in a Widget that needs the audio data to be displayed: +/// ``` +/// ... +/// late final Ticker ticker; +/// late final AudioData audioData; +/// late final double waveData; +/// late final double fftData; +/// +/// @override +/// void initState() { +/// super.initState(); +/// audioData = AudioData(GetSamplesFrom.player, GetSamplesKind.linear); +/// ticker = createTicker(_tick); +/// ticker.start(); +/// } +/// +/// @override +/// void dispose() { +/// ticker.stop(); +/// audioData.dispose(); +/// super.dispose(); +/// } +/// +/// void _tick(Duration elapsed) { +/// if (context.mounted) { +/// try { +/// audioData.updateSamples(); +/// setState(() {}); +/// } on Exception { +/// debugPrint('Player not initialized or visualization is not enabled!'); +/// } +/// } +/// } +/// ``` +/// Then in your "build" method, you can read the audio data: +/// ``` +/// try { +/// /// Use [getTexture] if you have inizialized [AudioData] +/// /// with [GetSamplesKind.texture] +/// ffiData = audioData.getLinearFft(i); +/// waveData = audioData.getLinearWave(i); +/// } on Exception { +/// ffiData = 0; +/// waveData = 0; +/// } +/// ``` +/// +/// To smooth FFT values use [SoLoud.instance.setFftSmoothing] or +/// [SoLoudCapture.instance.setCaptureFftSmoothing]. +/// +/// +// TODO(all): make AudioData singleton? +@experimental +class AudioData { + /// Initialize the way the audio data should be acquired. + AudioData( + this._getSamplesFrom, + this._getSamplesKind, + ) : ctrl = AudioDataCtrl() { + _init(); + ctrl.allocSamples(); + } + + void _init() { + switch (_getSamplesFrom) { + case GetSamplesFrom.player: + switch (_getSamplesKind) { + case GetSamplesKind.wave: + _updateCallback = ctrl.waveCallback; + case GetSamplesKind.linear: + _updateCallback = ctrl.textureCallback; + case GetSamplesKind.texture: + _updateCallback = ctrl.texture2DCallback; + } + case GetSamplesFrom.microphone: + switch (_getSamplesKind) { + case GetSamplesKind.wave: + _updateCallback = ctrl.captureWaveCallback; + case GetSamplesKind.linear: + _updateCallback = ctrl.captureAudioTextureCallback; + case GetSamplesKind.texture: + _updateCallback = ctrl.captureTexture2DCallback; + } + } + } + + /// The controller used to allocate, dispose and get audio data. + @internal + final AudioDataCtrl ctrl; + + /// Where to get audio samples. See [GetSamplesFrom]. + GetSamplesFrom _getSamplesFrom; + + /// The current device to acquire data. + GetSamplesFrom get getSamplesFrom => _getSamplesFrom; + + /// Kind of audio samples. See [GetSamplesKind]. + GetSamplesKind _getSamplesKind; + + /// The current type of data to acquire. + GetSamplesKind get getSamplesKind => _getSamplesKind; + + /// The callback used to get new audio samples. + /// This callback is used in [updateSamples] to avoid to + /// do the [GetSamplesFrom] and [GetSamplesKind] checks on every calls. + late void Function(AudioData) _updateCallback; + + /// Update the content of samples memory to be get with [getWave], + /// [getLinearFft], [getLinearWave] or [getTexture]. + /// + /// When using [GetSamplesFrom.microphone] throws + /// [SoLoudCaptureNotYetInitializededException] if the capture is + /// not initialized. + /// When using [GetSamplesFrom.player] throws [SoLoudNotInitializedException] + /// if the engine is not initialized. + /// When using [GetSamplesFrom.player] throws + /// [SoLoudVisualizationNotEnabledException] if the visualization + /// flag is not enableb. Please, Use `setVisualizationEnabled(true)` + /// when needed. + /// Throws [SoLoudNullPointerException] something is going wrong with the + /// player engine. Please, open an issue on + /// [GitHub](https://github.com/alnitak/flutter_soloud/issues) providing + /// a simple working example. + void updateSamples() { + _updateCallback(this); + } + + /// Changes the input device from which to retrieve audio data and its kind. + void changeType(GetSamplesFrom newFrom, GetSamplesKind newKind) { + _getSamplesKind = newKind; + _getSamplesFrom = newFrom; + _init(); + } + + /// Dispose the memory allocated to acquire audio data. + /// Must be called when there is no more need of [AudioData] otherwise memory + /// leaks will occur. + void dispose() { + ctrl.dispose(_getSamplesKind); + } + + /// Get the wave data at offset [offset]. + /// + /// Use this method to get data when using [GetSamplesKind.wave]. + /// The data is composed of 256 floats. + double getWave(SampleWave offset) { + if (_getSamplesKind != GetSamplesKind.wave) { + return 0; + } + + if (_getSamplesFrom == GetSamplesFrom.player && + !SoLoudController().soLoudFFI.getVisualizationEnabled()) { + throw const SoLoudVisualizationNotEnabledException(); + } + return ctrl.getWave(offset); + } + + /// Get the FFT audio data at offset [offset]. + /// + /// Use this method to get FFT data when using [GetSamplesKind.linear]. + /// The data is composed of 256 floats. + double getLinearFft(SampleLinear offset) { + if (_getSamplesKind != GetSamplesKind.linear) { + return 0; + } + + if (_getSamplesFrom == GetSamplesFrom.player && + !SoLoudController().soLoudFFI.getVisualizationEnabled()) { + throw const SoLoudVisualizationNotEnabledException(); + } + return ctrl.getLinearFft(offset); + } + + /// Get the wave audio data at offset [offset]. + /// + /// Use this method to get wave data when using [GetSamplesKind.linear]. + /// The data is composed of 256 floats. + double getLinearWave(SampleLinear offset) { + if (_getSamplesKind != GetSamplesKind.linear) { + return 0; + } + + if (_getSamplesFrom == GetSamplesFrom.player && + !SoLoudController().soLoudFFI.getVisualizationEnabled()) { + throw const SoLoudVisualizationNotEnabledException(); + } + return ctrl.getLinearWave(offset); + } + + /// Get the audio data at row [row] and column [column]. + /// Use this method to get data when using [GetSamplesKind.texture]. + /// This matrix represents 256 rows. Each rows is represented by 256 floats + /// of FFT data and 256 floats of wave data. + /// Each time the [AudioData.updateSamples] method is called, + /// the last row is discarded and the new one will be the first. + double getTexture(SampleRow row, SampleColumn column) { + if (_getSamplesKind != GetSamplesKind.texture) { + return 0; + } + + if (_getSamplesFrom == GetSamplesFrom.player && + !SoLoudController().soLoudFFI.getVisualizationEnabled()) { + throw const SoLoudVisualizationNotEnabledException(); + } + return ctrl.getTexture(_getSamplesFrom, row, column); + } +} diff --git a/lib/src/bindings/audio_data_extensions.dart b/lib/src/bindings/audio_data_extensions.dart new file mode 100644 index 0000000..c30a835 --- /dev/null +++ b/lib/src/bindings/audio_data_extensions.dart @@ -0,0 +1,110 @@ +/// The extension type for the `AudioData.get2D` method which accepts +/// the [value] value in 0~255 range. +extension type SampleRow._(int value) { + /// Constructs a valid row with [value]. + SampleRow(this.value) + : assert(value >= 0 && value <= 255, 'row must in 0~255 included range.'); + + /// Operator "*", clamp the resulting value. + SampleRow operator *(int other) { + final result = (other * value).clamp(0, 255); + return SampleRow(result); + } + + /// Operator "+", clamp the resulting value. + SampleRow operator +(int other) { + final result = (other + value).clamp(0, 255); + return SampleRow(result); + } + + /// Operator "-", clamp the resulting value. + SampleRow operator -(int other) { + final result = (other - value).clamp(0, 255); + return SampleRow(result); + } +} + +/// The extension type for the `AudioData.get2D` method which accepts +/// the [value] value in 0~511 range. +extension type SampleColumn._(int value) { + /// Constructs a valid column with [value]. + SampleColumn(this.value) + : assert(value >= 0 && value <= 511, 'row must in 0~511 included range.'); + + /// Operator "*", clamp the resulting value. + SampleColumn operator *(int other) { + final result = (other * value).clamp(0, 511); + return SampleColumn(result); + } + + /// Operator "+", clamp the resulting value. + SampleColumn operator +(int other) { + final result = (other + value).clamp(0, 511); + return SampleColumn(result); + } + + /// Operator "-", clamp the resulting value. + SampleColumn operator -(int other) { + final result = (other - value).clamp(0, 511); + return SampleColumn(result); + } +} + +/// The extension type for the `AudioData.getLinearFft` and +/// `AudioData.getLinearWave` method which accept +/// the [value] value in 0~255 range. +extension type SampleLinear._(int value) { + /// Constructs a valid offset with [value]. + SampleLinear(this.value) + : assert( + value >= 0 && value <= 255, + 'offset must in 0~255 included range.', + ); + + /// Operator "*", clamp the resulting value. + SampleLinear operator *(int other) { + final result = (other * value).clamp(0, 255); + return SampleLinear(result); + } + + /// Operator "+", clamp the resulting value. + SampleLinear operator +(int other) { + final result = (other + value).clamp(0, 255); + return SampleLinear(result); + } + + /// Operator "-", clamp the resulting value. + SampleLinear operator -(int other) { + final result = (other - value).clamp(0, 255); + return SampleLinear(result); + } +} + +/// The extension type for the `AudioData.getWave` +/// method which accepts the [value] value in 0~255 range. +extension type SampleWave._(int value) { + /// Constructs a valid offset with [value]. + SampleWave(this.value) + : assert( + value >= 0 && value <= 255, + 'offset must in 0~255 included range.', + ); + + /// Operator "*", clamp the resulting value. + SampleWave operator *(int other) { + final result = (other * value).clamp(0, 255); + return SampleWave(result); + } + + /// Operator "+", clamp the resulting value. + SampleWave operator +(int other) { + final result = (other + value).clamp(0, 255); + return SampleWave(result); + } + + /// Operator "-", clamp the resulting value. + SampleWave operator -(int other) { + final result = (other - value).clamp(0, 255); + return SampleWave(result); + } +} diff --git a/lib/src/bindings/audio_data_ffi.dart b/lib/src/bindings/audio_data_ffi.dart new file mode 100644 index 0000000..5941469 --- /dev/null +++ b/lib/src/bindings/audio_data_ffi.dart @@ -0,0 +1,81 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart' show calloc; +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/audio_data_extensions.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.dart'; +import 'package:flutter_soloud/src/enums.dart'; + +class AudioDataCtrl { + /// To reflect [AudioDataCtrl] for web. Not used with `dart:ffi` + final int _samplesPtr = 0; + int get samplesPtr => _samplesPtr; + + /// Where the FFT or wave data is stored. + late Pointer> samplesWave; + + /// Where the audio 2D data is stored. + late Pointer> samples2D; + + /// Where the audio 1D data is stored. + late Pointer samples1D; + + final void Function(AudioData) waveCallback = + SoLoudController().soLoudFFI.getWave; + + final PlayerErrors Function(AudioData) texture2DCallback = + SoLoudController().soLoudFFI.getAudioTexture2D; + + final void Function(AudioData) textureCallback = + SoLoudController().soLoudFFI.getAudioTexture; + + final void Function(AudioData) captureWaveCallback = + SoLoudController().captureFFI.getCaptureWave; + + final CaptureErrors Function(AudioData) captureTexture2DCallback = + SoLoudController().captureFFI.getCaptureAudioTexture2D; + + final void Function(AudioData) captureAudioTextureCallback = + SoLoudController().captureFFI.getCaptureAudioTexture; + + void allocSamples() { + samples2D = calloc(); + samples1D = calloc(512 * 4); + samplesWave = calloc(); + } + + void dispose( + GetSamplesKind getSamplesKind, + ) { + if (samplesWave != nullptr) calloc.free(samplesWave); + if (samples1D != nullptr) calloc.free(samples1D); + if (samples2D != nullptr) calloc.free(samples2D); + } + + double getWave(SampleWave offset) { + final val = Pointer.fromAddress(samplesWave.value.address); + if (val == nullptr) return 0; + return val[offset.value]; + } + + double getLinearFft(SampleLinear offset) { + return samples1D[offset.value]; + } + + double getLinearWave(SampleLinear offset) { + return samples1D[offset.value + 256]; + } + + double getTexture( + GetSamplesFrom getSamplesFrom, + SampleRow row, + SampleColumn column, + ) { + const stride = 512; + final val = samples2D.value; + if (val == nullptr) return 0; + return val[stride * row.value + column.value]; + } +} diff --git a/lib/src/bindings/audio_data_web.dart b/lib/src/bindings/audio_data_web.dart new file mode 100644 index 0000000..7342f67 --- /dev/null +++ b/lib/src/bindings/audio_data_web.dart @@ -0,0 +1,78 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/audio_data_extensions.dart'; +import 'package:flutter_soloud/src/bindings/js_extension.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.dart'; +import 'package:flutter_soloud/src/enums.dart'; + +class AudioDataCtrl { + late final int _samplesPtr; + int get samplesPtr => _samplesPtr; + + final void Function(AudioData) waveCallback = + SoLoudController().soLoudFFI.getWave; + + final void Function(AudioData) texture2DCallback = + SoLoudController().soLoudFFI.getAudioTexture2D; + + final void Function(AudioData) textureCallback = + SoLoudController().soLoudFFI.getAudioTexture; + + final void Function(AudioData) captureWaveCallback = + SoLoudController().captureFFI.getCaptureWave; + + final CaptureErrors Function(AudioData) captureTexture2DCallback = + SoLoudController().captureFFI.getCaptureAudioTexture2D; + + final void Function(AudioData) captureAudioTextureCallback = + SoLoudController().captureFFI.getCaptureAudioTexture; + + void allocSamples() { + /// This is the max amount of memory [_samplePtr] may need. This number + /// is needed when acquiring data with [getTexture] which is a matrix of + /// 256 rows and 512 columns of floats (4 bytes each). + _samplesPtr = wasmMalloc(512 * 256 * 4); + } + + void dispose( + GetSamplesKind getSamplesKind, + ) { + if (_samplesPtr != 0) { + wasmFree(_samplesPtr); + } + } + + double getWave(SampleWave offset) { + final samplePtr = wasmGetI32Value(_samplesPtr, '*'); + final data = wasmGetF32Value(samplePtr + offset.value * 4, 'float'); + return data; + } + + double getLinearFft(SampleLinear offset) { + final data = wasmGetF32Value(_samplesPtr + offset.value * 4, 'float'); + return data; + } + + double getLinearWave(SampleLinear offset) { + final data = + wasmGetF32Value(_samplesPtr + offset.value * 4 + 256 * 4, 'float'); + return data; + } + + double getTexture( + GetSamplesFrom getSamplesFrom, + SampleRow row, + SampleColumn column, + ) { + // final offset = samplesPtr + ((row.value * 256 + column.value) * 4); + // final data = wasmGetF32Value(offset, 'float'); + final double data; + if (getSamplesFrom == GetSamplesFrom.player) { + data = wasmGetTextureValue(row.value, column.value); + } else { + data = wasmGetCaptureTextureValue(row.value, column.value); + } + return data; + } +} diff --git a/lib/src/bindings/bindings_capture.dart b/lib/src/bindings/bindings_capture.dart new file mode 100644 index 0000000..3f2f2b3 --- /dev/null +++ b/lib/src/bindings/bindings_capture.dart @@ -0,0 +1,51 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/enums.dart'; +import 'package:meta/meta.dart'; + +export 'package:flutter_soloud/src/bindings/bindings_capture_ffi.dart' + if (dart.library.js_interop) 'package:flutter_soloud/src/bindings/bindings_capture_web.dart'; + +/// The experimenta functionality to use the microphone used by "SoLoudCapture". +@experimental +abstract class FlutterCapture { + @mustBeOverridden + List listCaptureDevices(); + + @mustBeOverridden + CaptureErrors initCapture(int deviceID); + + @mustBeOverridden + void disposeCapture(); + + @mustBeOverridden + bool isCaptureInited(); + + @mustBeOverridden + bool isCaptureStarted(); + + @mustBeOverridden + CaptureErrors startCapture(); + + @mustBeOverridden + CaptureErrors stopCapture(); + + @mustBeOverridden + void getCaptureFft(AudioData fft); + + @mustBeOverridden + void getCaptureWave(AudioData wave); + + @mustBeOverridden + void getCaptureAudioTexture(AudioData samples); + + @mustBeOverridden + CaptureErrors getCaptureAudioTexture2D(AudioData samples); + + @mustBeOverridden + double getCaptureTextureValue(int row, int column); + + @mustBeOverridden + CaptureErrors setCaptureFftSmoothing(double smooth); +} diff --git a/lib/src/bindings/bindings_capture_ffi.dart b/lib/src/bindings/bindings_capture_ffi.dart new file mode 100644 index 0000000..8575202 --- /dev/null +++ b/lib/src/bindings/bindings_capture_ffi.dart @@ -0,0 +1,229 @@ +import 'dart:ffi' as ffi; + +import 'package:ffi/ffi.dart'; +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/bindings_capture.dart'; +import 'package:flutter_soloud/src/enums.dart'; + +/// FFI bindings to capture with miniaudio. +class FlutterCaptureFfi extends FlutterCapture { + /// The symbols are looked up in [dynamicLibrary]. + FlutterCaptureFfi(ffi.DynamicLibrary dynamicLibrary) + : _lookup = dynamicLibrary.lookup; + + /// The symbols are looked up with [lookup]. + FlutterCaptureFfi.fromLookup( + ffi.Pointer Function(String symbolName) lookup, + ) : _lookup = lookup; + + /// Holds the symbol lookup function. + final ffi.Pointer Function(String symbolName) + _lookup; + + @override + List listCaptureDevices() { + final ret = []; + // ignore: omit_local_variable_types + final ffi.Pointer> deviceNames = + calloc(ffi.sizeOf>>() * 50); + // ignore: omit_local_variable_types + final ffi.Pointer> deviceIsDefault = + calloc(ffi.sizeOf>>() * 50); + // ignore: omit_local_variable_types + final ffi.Pointer nDevices = calloc(); + + _listCaptureDevices( + deviceNames, + deviceIsDefault, + nDevices, + ); + + final ndev = nDevices.value; + for (var i = 0; i < ndev; i++) { + final s1 = (deviceNames + i).value; + final s = s1.cast().toDartString(); + final n1 = (deviceIsDefault + i).value; + final n = n1.value; + ret.add(CaptureDevice(s, n == 1)); + } + + /// Free allocated memory done in C. + /// This work on all platforms but not on win. + // for (int i = 0; i < ndev; i++) { + // calloc.free(devices.elementAt(i).value.ref.name); + // calloc.free(devices.elementAt(i).value); + // } + _freeListCaptureDevices( + deviceNames, + deviceIsDefault, + ndev, + ); + + calloc + ..free(deviceNames) + ..free(nDevices); + return ret; + } + + late final _listCaptureDevicesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer>, + ffi.Pointer>, + ffi.Pointer, + )>>('listCaptureDevices'); + late final _listCaptureDevices = _listCaptureDevicesPtr.asFunction< + void Function( + ffi.Pointer>, + ffi.Pointer>, + ffi.Pointer, + )>(); + + late final _freeListCaptureDevicesPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer>, + ffi.Pointer>, + ffi.Int, + )>>('freeListCaptureDevices'); + late final _freeListCaptureDevices = _freeListCaptureDevicesPtr.asFunction< + void Function( + ffi.Pointer>, + ffi.Pointer>, + int, + )>(); + + @override + CaptureErrors initCapture(int deviceID) { + final e = _initCapture(deviceID); + return CaptureErrors.values[e]; + } + + late final _initCapturePtr = + _lookup>('initCapture'); + late final _initCapture = _initCapturePtr.asFunction(); + + @override + void disposeCapture() { + return _disposeCapture(); + } + + late final _disposeCapturePtr = + _lookup>('disposeCapture'); + late final _disposeCapture = _disposeCapturePtr.asFunction(); + + @override + bool isCaptureInited() { + return _isCaptureInited() == 1; + } + + late final _isCaptureInitedPtr = + _lookup>('isCaptureInited'); + late final _isCaptureInited = + _isCaptureInitedPtr.asFunction(); + + @override + bool isCaptureStarted() { + return _isCaptureStarted() == 1; + } + + late final _isCaptureStartedPtr = + _lookup>('isCaptureStarted'); + late final _isCaptureStarted = + _isCaptureStartedPtr.asFunction(); + + @override + CaptureErrors startCapture() { + return CaptureErrors.values[_startCapture()]; + } + + late final _startCapturePtr = + _lookup>('startCapture'); + late final _startCapture = _startCapturePtr.asFunction(); + + @override + CaptureErrors stopCapture() { + return CaptureErrors.values[_stopCapture()]; + } + + late final _stopCapturePtr = + _lookup>('stopCapture'); + late final _stopCapture = _stopCapturePtr.asFunction(); + + @override + void getCaptureFft(AudioData fft) { + return _getCaptureFft(fft.ctrl.samplesWave); + } + + late final _getCaptureFftPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer>, + )>>('getCaptureFft'); + late final _getCaptureFft = _getCaptureFftPtr + .asFunction>)>(); + + @override + void getCaptureWave(AudioData wave) { + return _getCaptureWave(wave.ctrl.samplesWave); + } + + late final _getCaptureWavePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer>, + )>>('getCaptureWave'); + late final _getCaptureWave = _getCaptureWavePtr + .asFunction>)>(); + + @override + void getCaptureAudioTexture(AudioData samples) { + return _getCaptureTexture(samples.ctrl.samples1D); + } + + late final _getCaptureTexturePtr = + _lookup)>>( + 'getCaptureTexture', + ); + late final _getCaptureTexture = + _getCaptureTexturePtr.asFunction)>(); + + @override + CaptureErrors getCaptureAudioTexture2D(AudioData samples) { + final ret = _getCaptureAudioTexture2D(samples.ctrl.samples2D); + return CaptureErrors.values[ret]; + } + + late final _getCaptureAudioTexture2DPtr = _lookup< + ffi + .NativeFunction>)>>( + 'getCaptureAudioTexture2D', + ); + late final _getCaptureAudioTexture2D = _getCaptureAudioTexture2DPtr + .asFunction>)>(); + + @override + double getCaptureTextureValue(int row, int column) { + return _getCaptureTextureValue(row, column); + } + + late final _getCaptureTextureValuePtr = + _lookup>( + 'getCaptureTextureValue', + ); + late final _getCaptureTextureValue = + _getCaptureTextureValuePtr.asFunction(); + + @override + CaptureErrors setCaptureFftSmoothing(double smooth) { + final ret = _setCaptureFftSmoothing(smooth); + return CaptureErrors.values[ret]; + } + + late final _setCaptureFftSmoothingPtr = + _lookup>( + 'setCaptureFftSmoothing', + ); + late final _setCaptureFftSmoothing = + _setCaptureFftSmoothingPtr.asFunction(); +} diff --git a/lib/src/bindings/bindings_capture_web.dart b/lib/src/bindings/bindings_capture_web.dart new file mode 100644 index 0000000..95d6cb9 --- /dev/null +++ b/lib/src/bindings/bindings_capture_web.dart @@ -0,0 +1,105 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/bindings_capture.dart'; +import 'package:flutter_soloud/src/bindings/js_extension.dart'; +import 'package:flutter_soloud/src/enums.dart'; + +class FlutterCaptureWeb extends FlutterCapture { + @override + List listCaptureDevices() { + /// allocate 50 device strings + final namesPtr = wasmMalloc(50 * 150); + final isDefaultPtr = wasmMalloc(50 * 4); + final nDevicesPtr = wasmMalloc(4); // 4 bytes for an int + + wasmListCaptureDevices( + namesPtr, + isDefaultPtr, + nDevicesPtr, + ); + + final nDevices = wasmGetI32Value(nDevicesPtr, '*'); + final devices = []; + for (var i = 0; i < nDevices; i++) { + final namePtr = wasmGetI32Value(namesPtr + i * 4, '*'); + final name = wasmUtf8ToString(namePtr); + final isDefault = + wasmGetI32Value(wasmGetI32Value(isDefaultPtr + i * 4, '*'), '*'); + + devices.add(CaptureDevice(name, isDefault == 1)); + } + + wasmFreeListCaptureDevices(namesPtr, isDefaultPtr, nDevices); + + wasmFree(nDevicesPtr); + wasmFree(isDefaultPtr); + wasmFree(namesPtr); + + return devices; + } + + @override + CaptureErrors initCapture(int deviceID) { + final e = wasmInitCapture(deviceID); + return CaptureErrors.values[e]; + } + + @override + void disposeCapture() { + return wasmDisposeCapture(); + } + + @override + bool isCaptureInited() { + return wasmIsCaptureInited() == 1; + } + + @override + bool isCaptureStarted() { + return wasmIsCaptureStarted() == 1; + } + + @override + CaptureErrors startCapture() { + return CaptureErrors.values[wasmStartCapture()]; + } + + @override + CaptureErrors stopCapture() { + return CaptureErrors.values[wasmStopCapture()]; + } + + @override + void getCaptureFft(AudioData fft) { + return wasmGetCaptureFft(fft.ctrl.samplesPtr); + } + + @override + void getCaptureWave(AudioData wave) { + return wasmGetCaptureWave(wave.ctrl.samplesPtr); + } + + @override + void getCaptureAudioTexture(AudioData samples) { + wasmGetCaptureAudioTexture(samples.ctrl.samplesPtr); + } + + @override + CaptureErrors getCaptureAudioTexture2D(AudioData samples) { + final e = wasmGetCaptureAudioTexture2D(samples.ctrl.samplesPtr); + return CaptureErrors.values[e]; + } + + @override + double getCaptureTextureValue(int row, int column) { + final value = wasmGetCaptureTextureValue(row, column); + return value; + } + + @override + CaptureErrors setCaptureFftSmoothing(double smooth) { + final e = wasmSetCaptureFftSmoothing(smooth); + return CaptureErrors.values[e]; + } +} diff --git a/lib/src/bindings/bindings_player.dart b/lib/src/bindings/bindings_player.dart new file mode 100644 index 0000000..79e8b9a --- /dev/null +++ b/lib/src/bindings/bindings_player.dart @@ -0,0 +1,752 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/enums.dart'; +import 'package:flutter_soloud/src/filter_params.dart'; +import 'package:flutter_soloud/src/sound_handle.dart'; +import 'package:flutter_soloud/src/sound_hash.dart'; +import 'package:meta/meta.dart'; + +export 'package:flutter_soloud/src/bindings/bindings_player_ffi.dart' + if (dart.library.js_interop) 'package:flutter_soloud/src/bindings/bindings_player_web.dart'; + +/// Abstract class defining the interface for the platform-specific +/// implementations. +abstract class FlutterSoLoud { + /// Controller to listen to voice ended events. + late final StreamController voiceEndedEventController = + StreamController.broadcast(); + + /// Listener for voices ended. + Stream get voiceEndedEvents => voiceEndedEventController.stream; + + /// Controller to listen to file loaded events. + /// Not used on the web. + late final StreamController> fileLoadedEventsController = + StreamController.broadcast(); + + /// Listener for file loaded. + /// Not used on the web. + Stream> get fileLoadedEvents => + fileLoadedEventsController.stream; + + /// Controller to listen to voice ended events. + /// Not used on the web. + @experimental + late final StreamController stateChangedController = + StreamController.broadcast(); + + /// listener for voices ended. + /// Not used on the web. + @experimental + Stream get stateChangedEvents => + stateChangedController.stream; + + /// Set Dart functions to call when an event occurs. + /// + /// On the web, only the `voiceEndedCallback` is supported. On the other + /// platform there are also `fileLoadedCallback` and `stateChangedCallback`. + @mustBeOverridden + void setDartEventCallbacks(); + + /// Initialize the player. Must be called before any other player functions. + /// + /// Returns [PlayerErrors.noError] if success. + @mustBeOverridden + PlayerErrors initEngine(); + + /// Must be called when the player is no more needed or when closing the app. + @mustBeOverridden + void deinit(); + + /// Gets the state of player + /// + /// Return true if initilized + @mustBeOverridden + bool isInited(); + + /// Load a new sound to be played once or multiple times later. + /// This is not supported on the web, use [loadMem] instead. + /// + /// After loading the file, the "_fileLoadedCallback" will call the + /// Dart function defined with "_setDartEventCallback" which gives back + /// the error and the new hash. + /// + /// [completeFileName] the complete file path. + /// [LoadMode] if `LoadMode.memory`, Soloud::wav will be used which loads + /// all audio data into memory. Used to prevent gaps or lags + /// when seeking/starting a sound (less CPU, more memory allocated). + /// If `LoadMode.disk` is used, the audio data is loaded + /// from the given file when needed (more CPU, less memory allocated). + /// See the [seek] note problem when using [LoadMode] = `LoadMode.disk`. + /// `soundHash` return hash of the sound. + @mustBeOverridden + void loadFile( + String completeFileName, + LoadMode mode, + ); + + /// Load a new sound stored into [buffer] as file bytes to be played once + /// or multiple times later. + /// This is used on the web instead of [loadFile] because the browsers are + /// not allowed to read files directly, but it works also on the other + /// platforms. + /// + /// [uniqueName] the unique name of the sound. Used only to have the [hash]. + /// [buffer] the audio data. These contains the audio file bytes. + @mustBeOverridden + ({PlayerErrors error, SoundHash soundHash}) loadMem( + String uniqueName, + Uint8List buffer, + LoadMode mode, + ); + + /// Load a new waveform to be played once or multiple times later. + /// + /// [waveform] + /// [superWave] + /// [scale] + /// [detune] + /// `soundHash` return hash of the sound. + /// Returns [PlayerErrors.noError] if success. + @mustBeOverridden + ({PlayerErrors error, SoundHash soundHash}) loadWaveform( + WaveForm waveform, + // ignore: avoid_positional_boolean_parameters + bool superWave, + double scale, + double detune, + ); + + /// Set the scale of an already loaded waveform identified by [hash]. + /// + /// [hash] the unique sound hash of a waveform sound. + /// [newScale] the new scale of the wave. + @mustBeOverridden + void setWaveformScale(SoundHash hash, double newScale); + + /// Set the detune of an already loaded waveform identified by [hash]. + /// + /// [hash] the unique sound hash of a waveform sound. + /// [newDetune] the new detune of the wave. + @mustBeOverridden + void setWaveformDetune(SoundHash hash, double newDetune); + + /// Set a new frequency of an already loaded waveform identified by [hash]. + /// + /// [hash] the unique sound hash of a waveform sound. + /// [newFreq] the new frequence of the wave. + @mustBeOverridden + void setWaveformFreq(SoundHash hash, double newFreq); + + /// Set a new frequence of an already loaded waveform identified by [hash]. + /// + /// [hash] the unique sound hash of a waveform sound. + /// [superwave] 1 if using the super wave. + @mustBeOverridden + void setWaveformSuperWave(SoundHash hash, int superwave); + + /// Set a new wave form of an already loaded waveform identified by [hash]. + /// + /// [hash] the unique sound hash of a waveform sound. + /// [newWaveform] the new kind of [WaveForm] to be used. + @mustBeOverridden + void setWaveform(SoundHash hash, WaveForm newWaveform); + + /// Speech the text given. + /// + /// [textToSpeech] the text to be spoken. + /// Returns [PlayerErrors.noError] if success and handle sound identifier. + // TODO(marco): add other T2S parameters + @mustBeOverridden + ({PlayerErrors error, SoundHandle handle}) speechText(String textToSpeech); + + /// Switch pause state of an already loaded sound identified by [handle]. + /// + /// [handle] the sound handle. + @mustBeOverridden + void pauseSwitch(SoundHandle handle); + + /// Pause or unpause already loaded sound identified by [handle]. + /// + /// [handle] the sound handle. + /// [pause] the new state. + @mustBeOverridden + void setPause(SoundHandle handle, int pause); + + /// Gets the pause state. + /// + /// [handle] the sound handle. + /// Return true if paused. + @mustBeOverridden + bool getPause(SoundHandle handle); + + /// Set a sound's relative play speed. + /// Setting the value to 0 will cause undefined behavior, likely a crash. + /// Change the relative play speed of a sample. 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. + /// + /// [handle] the sound handle. + /// [speed] the new speed. + @mustBeOverridden + void setRelativePlaySpeed(SoundHandle handle, double speed); + + /// Return the current play speed. + /// + /// [handle] the sound handle. + @mustBeOverridden + double getRelativePlaySpeed(SoundHandle handle); + + /// Play already loaded sound identified by [soundHash]. + /// + /// [soundHash] the unique sound hash of a sound. + /// [volume] 1.0 full volume. + /// [pan] 0.0 centered. + /// [paused] false not paused. + /// [looping] whether to start the sound in looping state. + /// [loopingStartAt] If looping is enabled, the loop point is, by default, + /// the start of the stream. The loop start point can be set with this + /// parameter, and current loop point can be queried with `getLoopingPoint()` + /// and changed by `setLoopingPoint()`. + /// Return the error if any and a new `newHandle` of this sound. + @mustBeOverridden + ({PlayerErrors error, SoundHandle newHandle}) play( + SoundHash soundHash, { + double volume = 1, + double pan = 0, + bool paused = false, + bool looping = false, + Duration loopingStartAt = Duration.zero, + }); + + /// Stop already loaded sound identified by [handle] and clear it. + /// + /// [handle] the sound handle. + @mustBeOverridden + void stop(SoundHandle handle); + + /// Stop all handles of the already loaded sound identified + /// by [soundHash] and dispose it. + /// + /// [soundHash] the unique sound hash of a sound. + @mustBeOverridden + void disposeSound(SoundHash soundHash); + + /// Dispose all sounds already loaded. + @mustBeOverridden + void disposeAllSound(); + + /// Query whether a sound is set to loop. + /// + /// [handle] the sound handle. + /// Returns true if flagged for looping. + @mustBeOverridden + bool getLooping(SoundHandle handle); + + /// This function can be used to set a sample to play on repeat, + /// instead of just playing it once. + /// + /// [handle] the sound handle. + /// [enable] enable or not the looping. + @mustBeOverridden + // ignore: avoid_positional_boolean_parameters + void setLooping(SoundHandle handle, bool enable); + + /// Get sound loop point value. + /// + /// [handle] the sound handle. + /// Returns the duration. + @mustBeOverridden + Duration getLoopPoint(SoundHandle handle); + + /// Set sound loop point value. + /// + /// [handle] the sound handle. + /// [timestamp] the time in which the loop will restart. + @mustBeOverridden + void setLoopPoint(SoundHandle handle, Duration timestamp); + + // TODO(marco): implement Soloud.getLoopCount() also? + + /// Enable or disable visualization. + /// Not yet supported on the web. + /// + /// [enabled] whether to enable or disable. + @mustBeOverridden + // ignore: avoid_positional_boolean_parameters + void setVisualizationEnabled(bool enabled); + + /// Get visualization state. + /// Not yet supported on the web. + /// + /// Return true if enabled. + @mustBeOverridden + bool getVisualizationEnabled(); + + /// Returns valid data only if VisualizationEnabled is true. + /// Not yet supported on the web. + /// + /// [fft] on all platforms web excluded, the [fft] type is `Pointer`. + /// Return a 256 float array int the [fft] pointer containing FFT data. + @mustBeOverridden + void getFft(AudioData fft); + + /// Returns valid data only if VisualizationEnabled is true + /// + /// [wave] on all platforms web excluded, the [wave] type is `Pointer`. + /// Return a 256 float array int the [wave] pointer containing audio data. + @mustBeOverridden + void getWave(AudioData wave); + + /// Smooth FFT data. + /// Not yet supported on the web. + /// + /// When new data is read and the values are decreasing, the new value will be + /// decreased with an amplitude between the old and the new value. + /// This will result on a less shaky visualization. + /// + /// [smooth] must be in the [0.0 ~ 1.0] range. + /// 0 = no smooth + /// 1 = full smooth + /// the new value is calculated with: + /// newFreq = smooth * oldFreq + (1 - smooth) * newFreq + @mustBeOverridden + void setFftSmoothing(double smooth); + + /// Return in [samples] a 512 float array. + /// The first 256 floats represent the FFT frequencies data [>=0.0]. + /// The other 256 floats represent the wave data (amplitude) [-1.0~1.0]. + /// Not yet supported on the web. + /// + /// [samples] on all platforms web excluded, the [samples] type is + /// `Pointer`. + @mustBeOverridden + void getAudioTexture(AudioData samples); + + /// Return a floats matrix of 256x512 + /// Every row are composed of 256 FFT values plus 256 of wave data + /// Every time is called, a new row is stored in the + /// first row and all the previous rows are shifted + /// up and the last one will be lost. + /// + /// [samples] on all platforms web excluded, the [samples] type is + /// `Pointer>`. + @mustBeOverridden + PlayerErrors getAudioTexture2D(AudioData samples); + + /// Get the value in the texture2D matrix at the given coordinates. + @mustBeOverridden + double getTextureValue(int row, int column); + + /// Get the sound length. + /// + /// [soundHash] the sound hash. + /// Returns sound length. + @mustBeOverridden + Duration getLength(SoundHash soundHash); + + /// Seek playing to [time] position. + /// + /// [time] the time position to seek to. + /// [handle] the sound handle. + /// Returns [PlayerErrors.noError] if success. + /// + /// NOTE: when seeking an MP3 file loaded using `mode`=`LoadMode.disk` the + /// seek operation is performed but there will be delays. This occurs because + /// the MP3 codec must compute each frame length to gain a new position. + /// The problem is explained in souloud_wavstream.cpp + /// in `WavStreamInstance::seek` function. + /// + /// This mode is useful ie for background music, not for a music player + /// where a seek slider for MP3s is a must. + /// If you need to seek MP3s without lags, please, use + /// `mode`=`LoadMode.memory` instead or other supported audio formats! + @mustBeOverridden + int seek(SoundHandle handle, Duration time); + + /// Get current sound position.. + /// + /// [handle] the sound handle. + /// Returns time position. + @mustBeOverridden + Duration getPosition(SoundHandle handle); + + /// Get current Global volume. + /// + /// Returns the volume. + @mustBeOverridden + double getGlobalVolume(); + + /// Set current Global volume. + /// + /// Returns [PlayerErrors.noError] if success. + @mustBeOverridden + int setGlobalVolume(double volume); + + /// Get current [handle] volume. + /// + /// Returns the volume. + @mustBeOverridden + double getVolume(SoundHandle handle); + + /// Set current [handle] volume. + /// + /// Returns [PlayerErrors.noError] if success. + @mustBeOverridden + int setVolume(SoundHandle handle, double volume); + + /// Get a sound's current pan setting. + /// + /// [handle] the sound handle. + /// Returns the range of the pan values is -1 to 1, where -1 is left, 0 is + /// middle and and 1 is right. + @mustBeOverridden + double getPan(SoundHandle handle); + + /// Set a sound's current pan setting. + /// + /// [handle] the sound handle. + /// [pan] the range of the pan values is -1 to 1, where -1 is left, 0 is + /// middle and and 1 is right. + @mustBeOverridden + void setPan(SoundHandle handle, double pan); + + /// Set the left/right volumes directly. + /// Note that this does not affect the value returned by getPan. + /// + /// [handle] the sound handle. + /// [panLeft] value for the left pan. + /// [panRight] value for the right pan. + @mustBeOverridden + void setPanAbsolute(SoundHandle handle, double panLeft, double panRight); + + /// Check if the [handle] is still valid. + /// + /// [handle] handle to check. + /// Return true if it still exists. + @mustBeOverridden + bool getIsValidVoiceHandle(SoundHandle handle); + + /// Returns the number of concurrent sounds that are playing at the moment. + @mustBeOverridden + int getActiveVoiceCount(); + + /// Returns the number of concurrent sounds that are playing a + /// specific audio source. + @mustBeOverridden + int countAudioSource(SoundHash soundHash); + + /// Returns the number of voices the application has told SoLoud to play. + @mustBeOverridden + int getVoiceCount(); + + /// Get a sound's protection state. + @mustBeOverridden + bool getProtectVoice(SoundHandle handle); + + /// Set a sound's protection state. + /// + /// Normally, if you try to play more sounds than there are voices, + /// SoLoud will kill off the oldest playing sound to make room. + /// This will most likely be your background music. This can be worked + /// around by protecting the sound. + /// If all voices are protected, the result will be undefined. + /// + /// [handle] handle to check. + /// [protect] whether to protect or not. + @mustBeOverridden + // ignore: avoid_positional_boolean_parameters + void setProtectVoice(SoundHandle handle, bool protect); + + /// Get the current maximum active voice count. + @mustBeOverridden + int getMaxActiveVoiceCount(); + + /// Set the current maximum active voice count. + /// If voice count is higher than the maximum active voice count, + /// SoLoud will pick the ones with the highest volume to actually play. + /// [maxVoiceCount] the max concurrent sounds that can be played. + /// + /// NOTE: The number of concurrent voices is limited, as having unlimited + /// voices would cause performance issues, as well as lead to unnecessary + /// clipping. The default number of concurrent voices is 16, but this can be + /// adjusted at runtime. The hard maximum number is 4095, but if more are + /// required, SoLoud can be modified to support more. But seriously, if you + /// need more than 4095 sounds at once, you're probably going to make + /// some serious changes in any case. + @mustBeOverridden + void setMaxActiveVoiceCount(int maxVoiceCount); + + // /////////////////////////////////////// + // faders + // /////////////////////////////////////// + + /// Smoothly change the global volume over specified [duration]. + @mustBeOverridden + int fadeGlobalVolume(double to, Duration duration); + + /// Smoothly change a channel's volume over specified [duration]. + @mustBeOverridden + int fadeVolume(SoundHandle handle, double to, Duration duration); + + /// Smoothly change a channel's pan setting over specified [duration]. + @mustBeOverridden + int fadePan(SoundHandle handle, double to, Duration duration); + + /// Smoothly change a channel's relative play speed over specified time. + @mustBeOverridden + int fadeRelativePlaySpeed(SoundHandle handle, double to, Duration time); + + /// After specified [duration], pause the channel. + @mustBeOverridden + int schedulePause(SoundHandle handle, Duration duration); + + /// After specified time, stop the channel. + @mustBeOverridden + int scheduleStop(SoundHandle handle, Duration duration); + + /// Set fader to oscillate the volume at specified frequency. + @mustBeOverridden + int oscillateVolume( + SoundHandle handle, + double from, + double to, + Duration time, + ); + + /// Set fader to oscillate the panning at specified frequency. + @mustBeOverridden + int oscillatePan(SoundHandle handle, double from, double to, Duration time); + + /// Set fader to oscillate the relative play speed at specified frequency. + @mustBeOverridden + int oscillateRelativePlaySpeed( + SoundHandle handle, + double from, + double to, + Duration time, + ); + + /// Set fader to oscillate the global volume at specified frequency. + @mustBeOverridden + int oscillateGlobalVolume(double from, double to, Duration time); + + // /////////////////////////////////////// + // Filters + // /////////////////////////////////////// + + /// Check if the given filter is active or not. + /// + /// [filterType] filter to check. + /// Returns [PlayerErrors.noError] if no errors and the index of + /// the active filter (-1 if the filter is not active). + @mustBeOverridden + ({PlayerErrors error, int index}) isFilterActive(FilterType filterType); + + /// Get parameters names of the given filter. + /// + /// [filterType] filter to get param names. + /// Returns [PlayerErrors.noError] if no errors and the list of param names. + @mustBeOverridden + ({PlayerErrors error, List names}) getFilterParamNames( + FilterType filterType, + ); + + /// Add the filter [filterType]. + /// + /// [filterType] filter to add. + /// Returns: + /// [PlayerErrors.noError] if no errors. + /// [PlayerErrors.filterNotFound] if the [filterType] does not exits. + /// [PlayerErrors.filterAlreadyAdded] when trying to add an already + /// added filter. + /// [PlayerErrors.maxNumberOfFiltersReached] when the maximum number of + /// filters has been reached (default is 8). + @mustBeOverridden + PlayerErrors addGlobalFilter(FilterType filterType); + + /// Remove the filter [filterType]. + /// + /// [filterType] filter to remove. + /// Returns [PlayerErrors.noError] if no errors. + @mustBeOverridden + int removeGlobalFilter(FilterType filterType); + + /// Set the effect parameter with id [attributeId] of [filterType] + /// with [value] value. + /// + /// [filterType] filter to modify a param. + /// Returns [PlayerErrors.noError] if no errors. + @mustBeOverridden + int setFilterParams(FilterType filterType, int attributeId, double value); + + /// Get the effect parameter with id [attributeId] of [filterType]. + /// + /// [filterType] the filter to modify a parameter. + /// Returns the value of the parameter. + @mustBeOverridden + double getFilterParams(FilterType filterType, int attributeId); + + // /////////////////////////////////////// + // 3D audio methods + // /////////////////////////////////////// + + /// play3d() is the 3d version of the play() call. + /// + /// [posX], [posY], [posZ] are the audio source position coordinates. + /// [velX], [velY], [velZ] are the audio source velocity. + /// [looping] whether to start the sound in looping state. + /// [loopingStartAt] If looping is enabled, the loop point is, by default, + /// the start of the stream. The loop start point can be set with this + /// parameter, and current loop point can be queried with `getLoopingPoint()` + /// and changed by `setLoopingPoint()`. + /// Returns the handle of the sound, 0 if error. + @mustBeOverridden + ({PlayerErrors error, SoundHandle newHandle}) play3d( + SoundHash soundHash, + double posX, + double posY, + double posZ, { + double velX = 0, + double velY = 0, + double velZ = 0, + double volume = 1, + bool paused = false, + bool looping = false, + Duration loopingStartAt = Duration.zero, + }); + + /// Since SoLoud has no knowledge of the scale of your coordinates, + /// you may need to adjust the speed of sound for these effects + /// to work correctly. The default value is 343, which assumes + /// that your world coordinates are in meters (where 1 unit is 1 meter), + /// and that the environment is dry air at around 20 degrees Celsius. + @mustBeOverridden + void set3dSoundSpeed(double speed); + + /// Get the sound speed. + @mustBeOverridden + double get3dSoundSpeed(); + + /// You can set the position, at-vector, up-vector and velocity parameters + /// of the 3d audio listener with one call. + @mustBeOverridden + void set3dListenerParameters( + double posX, + double posY, + double posZ, + double atX, + double atY, + double atZ, + double upX, + double upY, + double upZ, + double velocityX, + double velocityY, + double velocityZ, + ); + + /// You can set the position parameter of the 3d audio listener. + @mustBeOverridden + void set3dListenerPosition(double posX, double posY, double posZ); + + /// You can set the "at" vector parameter of the 3d audio listener. + @mustBeOverridden + void set3dListenerAt(double atX, double atY, double atZ); + + /// You can set the "up" vector parameter of the 3d audio listener. + @mustBeOverridden + void set3dListenerUp(double upX, double upY, double upZ); + + /// You can set the listener's velocity vector parameter. + @mustBeOverridden + void set3dListenerVelocity( + double velocityX, + double velocityY, + double velocityZ, + ); + + /// You can set the position and velocity parameters of a live + /// 3d audio source with one call. + @mustBeOverridden + void set3dSourceParameters( + SoundHandle handle, + double posX, + double posY, + double posZ, + double velocityX, + double velocityY, + double velocityZ, + ); + + /// You can set the position parameters of a live 3d audio source. + @mustBeOverridden + void set3dSourcePosition( + SoundHandle handle, + double posX, + double posY, + double posZ, + ); + + /// You can set the velocity parameters of a live 3d audio source. + @mustBeOverridden + void set3dSourceVelocity( + SoundHandle handle, + double velocityX, + double velocityY, + double velocityZ, + ); + + /// You can set the minimum and maximum distance parameters + /// of a live 3d audio source. + @mustBeOverridden + void set3dSourceMinMaxDistance( + SoundHandle handle, + double minDistance, + double maxDistance, + ); + + /// You can change the attenuation model and rolloff factor parameters of + /// a live 3d audio source. + /// + /// NO_ATTENUATION No attenuation + /// INVERSE_DISTANCE Inverse distance attenuation model + /// LINEAR_DISTANCE Linear distance attenuation model + /// EXPONENTIAL_DISTANCE Exponential distance attenuation model + /// + /// see https://solhsa.com/soloud/concepts3d.html + @mustBeOverridden + void set3dSourceAttenuation( + SoundHandle handle, + int attenuationModel, + double attenuationRolloffFactor, + ); + + /// You can change the doppler factor of a live 3d audio source. + @mustBeOverridden + void set3dSourceDopplerFactor(SoundHandle handle, double dopplerFactor); +} + +/// Used for easier conversion from [double] to [Duration]. +extension DoubleToDuration on double { + /// Convert the double. + Duration toDuration() { + return Duration( + microseconds: (this * Duration.microsecondsPerSecond).round(), + ); + } +} + +/// Used for easier conversion from [Duration] to [double]. +extension DurationToDouble on Duration { + /// Convert the duration. + double toDouble() { + return inMicroseconds / Duration.microsecondsPerSecond; + } +} diff --git a/lib/src/bindings_player_ffi.dart b/lib/src/bindings/bindings_player_ffi.dart similarity index 69% rename from lib/src/bindings_player_ffi.dart rename to lib/src/bindings/bindings_player_ffi.dart index f3d1435..2a8dd3b 100644 --- a/lib/src/bindings_player_ffi.dart +++ b/lib/src/bindings/bindings_player_ffi.dart @@ -4,12 +4,14 @@ // ignore_for_file: avoid_positional_boolean_parameters,require_trailing_commas // ignore_for_file: public_member_api_docs -import 'dart:async'; import 'dart:ffi' as ffi; import 'dart:typed_data'; import 'package:ffi/ffi.dart'; +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/bindings_player.dart'; import 'package:flutter_soloud/src/enums.dart'; +import 'package:flutter_soloud/src/filter_params.dart'; import 'package:flutter_soloud/src/sound_handle.dart'; import 'package:flutter_soloud/src/sound_hash.dart'; import 'package:logging/logging.dart'; @@ -47,7 +49,8 @@ typedef DartdartStateChangedCallbackTFunction = void Function( ffi.Pointer); /// FFI bindings to SoLoud -class FlutterSoLoudFfi { +@internal +class FlutterSoLoudFfi extends FlutterSoLoud { static final Logger _log = Logger('flutter_soloud.FlutterSoLoudFfi'); /// Holds the symbol lookup function. @@ -65,39 +68,10 @@ class FlutterSoLoudFfi { ffi.Pointer Function(String symbolName) lookup, ) : _lookup = lookup; - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// + // //////////////////////////////////////////////// + // Callbacks impl + // //////////////////////////////////////////////// - /// When allocating memory in C code, more attention must be given when - /// we are on Windows OS. It's not good to call `calloc.free()` because - /// Windows could use different allocating methods for this and the same - /// must be used freeing it. `calloc.free()` use the standard `free()` and - /// doesn't have problems using it in other OSes. - void nativeFree(ffi.Pointer pointer) { - return _nativeFree(pointer); - } - - late final _nativeFreePtr = - _lookup)>>( - 'nativeFree'); - late final _nativeFree = - _nativeFreePtr.asFunction)>(); - - /// Controller to listen to voice ended events. - @internal - late final StreamController voiceEndedEventController = - StreamController.broadcast(); - - /// listener for voices ended. - @internal - Stream get voiceEndedEvents => voiceEndedEventController.stream; - - /// void _voiceEndedCallback(ffi.Pointer handle) { _log.finest(() => 'VOICE ENDED EVENT handle: ${handle.value}'); voiceEndedEventController.add(handle.value); @@ -106,16 +80,6 @@ class FlutterSoLoudFfi { nativeFree(handle.cast()); } - /// Controller to listen to file loaded events. - @internal - late final StreamController> fileLoadedEventsController = - StreamController.broadcast(); - - /// listener for file loaded. - @internal - Stream> get fileLoadedEvents => - fileLoadedEventsController.stream; - /// void _fileLoadedCallback( ffi.Pointer error, @@ -139,16 +103,6 @@ class FlutterSoLoudFfi { nativeFree(hash.cast()); } - /// Controller to listen to voice ended events. - @internal - late final StreamController stateChangedController = - StreamController.broadcast(); - - /// listener for voices ended. - @internal - Stream get stateChangedEvents => - stateChangedController.stream; - void _stateChangedCallback(ffi.Pointer state) { final s = PlayerStateNotification.values[state.value]; // Must free a pointer made on cpp. On Windows this must be freed @@ -158,8 +112,7 @@ class FlutterSoLoudFfi { stateChangedController.add(s); } - /// Set a Dart function to call when a sound ends. - /// + @override void setDartEventCallbacks() { // Create a NativeCallable for the Dart functions final nativeVoiceEndedCallable = @@ -190,16 +143,26 @@ class FlutterSoLoudFfi { void Function(DartVoiceEndedCallbackT, DartFileLoadedCallbackT, DartStateChangedCallbackT)>(); - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////// - /// Initialize the player. Must be called before any other player functions - /// - /// Returns [PlayerErrors.noError] if success + // //////////////////////////////////////////////// + // Navtive bindings + // //////////////////////////////////////////////// + + /// When allocating memory in C code, more attention must be given when + /// we are on Windows OS. It's not good to call `calloc.free()` because + /// Windows could use different allocating methods for this and the same + /// must be used freeing it. `calloc.free()` use the standard `free()` and + /// doesn't have problems using it in other OSes. + void nativeFree(ffi.Pointer pointer) { + return _nativeFree(pointer); + } + + late final _nativeFreePtr = + _lookup)>>( + 'nativeFree'); + late final _nativeFree = + _nativeFreePtr.asFunction)>(); + + @override PlayerErrors initEngine() { return PlayerErrors.values[_initEngine()]; } @@ -208,9 +171,7 @@ class FlutterSoLoudFfi { _lookup>('initEngine'); late final _initEngine = _initEnginePtr.asFunction(); - /// Must be called when there is no more need of the player - /// or when closing the app - /// + @override void deinit() { return _dispose(); } @@ -219,9 +180,7 @@ class FlutterSoLoudFfi { _lookup>('dispose'); late final _dispose = _disposePtr.asFunction(); - /// Gets the state of player - /// - /// Return true if initilized + @override bool isInited() { return _isInited() == 1; } @@ -231,20 +190,10 @@ class FlutterSoLoudFfi { ); late final _isInited = _isInitedPtr.asFunction(); - /// Load a new sound to be played once or multiple times later. - /// /// After loading the file, the [_fileLoadedCallback] will call the /// Dart function defined with [_setDartEventCallback] which gives back /// the error and the new hash. - /// - /// [completeFileName] the complete file path. - /// [LoadMode] if `LoadMode.memory`, Soloud::wav will be used which loads - /// all audio data into memory. Used to prevent gaps or lags - /// when seeking/starting a sound (less CPU, more memory allocated). - /// If `LoadMode.disk` is used, the audio data is loaded - /// from the given file when needed (more CPU, less memory allocated). - /// See the [seek] note problem when using [LoadMode] = `LoadMode.disk`. - /// `soundHash` return hash of the sound. + @override void loadFile( String completeFileName, LoadMode mode, @@ -268,16 +217,11 @@ class FlutterSoLoudFfi { late final _loadFile = _loadFilePtr.asFunction, int)>(); - /// Load a new sound stored into [buffer] as file bytes to be played once - /// or multiple times later. - /// Use this on web because the browsers are not allowed to read - /// files directly. - /// - /// [uniqueName] the unique name of the sound. Used only to have the [hash]. - /// [buffer] the audio data. These contains the audio file bytes. + @override ({PlayerErrors error, SoundHash soundHash}) loadMem( String uniqueName, Uint8List buffer, + LoadMode mode, ) { // ignore: omit_local_variable_types final ffi.Pointer hash = @@ -292,6 +236,7 @@ class FlutterSoLoudFfi { uniqueName.toNativeUtf8().cast(), bufferPtr, buffer.length, + mode == LoadMode.memory ? 1 : 0, hash, ); final soundHash = SoundHash(hash.value); @@ -303,19 +248,12 @@ class FlutterSoLoudFfi { late final _loadMemPtr = _lookup< ffi.NativeFunction< ffi.Int32 Function(ffi.Pointer, ffi.Pointer, - ffi.Int, ffi.Pointer)>>('loadMem'); + ffi.Int, ffi.Int, ffi.Pointer)>>('loadMem'); late final _loadMem = _loadMemPtr.asFunction< - int Function(ffi.Pointer, ffi.Pointer, int, + int Function(ffi.Pointer, ffi.Pointer, int, int, ffi.Pointer)>(); - /// Load a new waveform to be played once or multiple times later - /// - /// [waveform] - /// [superWave] - /// [scale] - /// [detune] - /// `soundHash` return hash of the sound - /// Returns [PlayerErrors.noError] if success + @override ({PlayerErrors error, SoundHash soundHash}) loadWaveform( WaveForm waveform, bool superWave, @@ -345,10 +283,7 @@ class FlutterSoLoudFfi { late final _loadWaveform = _loadWaveformPtr.asFunction< int Function(int, int, double, double, ffi.Pointer)>(); - /// Set the scale of an already loaded waveform identified by [hash] - /// - /// [hash] the unique sound hash of a waveform sound - /// [newScale] + @override void setWaveformScale(SoundHash hash, double newScale) { return _setWaveformScale(hash.hash, newScale); } @@ -359,10 +294,7 @@ class FlutterSoLoudFfi { late final _setWaveformScale = _setWaveformScalePtr.asFunction(); - /// Set the detune of an already loaded waveform identified by [hash] - /// - /// [hash] the unique sound hash of a waveform sound - /// [newDetune] + @override void setWaveformDetune(SoundHash hash, double newDetune) { return _setWaveformDetune(hash.hash, newDetune); } @@ -373,10 +305,7 @@ class FlutterSoLoudFfi { late final _setWaveformDetune = _setWaveformDetunePtr.asFunction(); - /// Set a new frequency of an already loaded waveform identified by [hash] - /// - /// [hash] the unique sound hash of a waveform sound - /// [newFreq] + @override void setWaveformFreq(SoundHash hash, double newFreq) { return _setWaveformFreq(hash.hash, newFreq); } @@ -387,10 +316,7 @@ class FlutterSoLoudFfi { late final _setWaveformFreq = _setWaveformFreqPtr.asFunction(); - /// Set a new frequence of an already loaded waveform identified by [hash] - /// - /// [hash] the unique sound hash of a waveform sound - /// [superwave] + @override void setWaveformSuperWave(SoundHash hash, int superwave) { return _setSuperWave(hash.hash, superwave); } @@ -401,18 +327,7 @@ class FlutterSoLoudFfi { late final _setSuperWave = _setSuperWavePtr.asFunction(); - /// Set a new wave form of an already loaded waveform identified by [hash] - /// - /// [hash] the unique sound hash of a waveform sound - /// [newWaveform] WAVE_SQUARE = 0, - /// WAVE_SAW, - /// WAVE_SIN, - /// WAVE_TRIANGLE, - /// WAVE_BOUNCE, - /// WAVE_JAWS, - /// WAVE_HUMPS, - /// WAVE_FSQUARE, - /// WAVE_FSAW + @override void setWaveform(SoundHash hash, WaveForm newWaveform) { return _setWaveform(hash.hash, newWaveform.index); } @@ -423,11 +338,7 @@ class FlutterSoLoudFfi { late final _setWaveform = _setWaveformPtr.asFunction(); - /// Speech the text given - /// - /// [textToSpeech] - /// Returns PlayerErrors.noError if success and handle sound identifier - // TODO(marco): add other T2S parameters + @override ({PlayerErrors error, SoundHandle handle}) speechText(String textToSpeech) { // ignore: omit_local_variable_types final ffi.Pointer handle = calloc(); @@ -450,9 +361,7 @@ class FlutterSoLoudFfi { late final _speechText = _speechTextPtr.asFunction< int Function(ffi.Pointer, ffi.Pointer)>(); - /// Switch pause state of an already loaded sound identified by [handle] - /// - /// [handle] the sound handle + @override void pauseSwitch(SoundHandle handle) { return _pauseSwitch(handle.id); } @@ -463,10 +372,7 @@ class FlutterSoLoudFfi { ); late final _pauseSwitch = _pauseSwitchPtr.asFunction(); - /// Pause or unpause already loaded sound identified by [handle] - /// - /// [handle] the sound handle - /// [pause] the sound handle + @override void setPause(SoundHandle handle, int pause) { return _setPause(handle.id, pause); } @@ -476,10 +382,7 @@ class FlutterSoLoudFfi { 'setPause'); late final _setPause = _setPausePtr.asFunction(); - /// Gets the pause state - /// - /// [handle] the sound handle - /// Return true if paused + @override bool getPause(SoundHandle handle) { return _getPause(handle.id) == 1; } @@ -490,18 +393,7 @@ class FlutterSoLoudFfi { ); late final _getPause = _getPausePtr.asFunction(); - /// Set a sound's relative play speed. - /// Setting the value to 0 will cause undefined behavior, likely a crash. - /// Change the relative play speed of a sample. 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. - /// - /// [handle] the sound handle - /// [speed] the new speed + @override void setRelativePlaySpeed(SoundHandle handle, double speed) { return _setRelativePlaySpeed(handle.id, speed); } @@ -515,6 +407,7 @@ class FlutterSoLoudFfi { /// Return the current play speed. /// /// [handle] the sound handle + @override double getRelativePlaySpeed(SoundHandle handle) { return _getRelativePlaySpeed(handle.id); } @@ -525,18 +418,7 @@ class FlutterSoLoudFfi { late final _getRelativePlaySpeed = _getRelativePlaySpeedPtr.asFunction(); - /// Play already loaded sound identified by [soundHash] - /// - /// [soundHash] the unique sound hash of a sound - /// [volume] 1.0 full volume - /// [pan] 0.0 centered - /// [paused] false not paused - /// [looping] whether to start the sound in looping state. - /// [loopingStartAt] If looping is enabled, the loop point is, by default, - /// the start of the stream. The loop start point can be set with this - /// parameter, and current loop point can be queried with `getLoopingPoint()` - /// and changed by `setLoopingPoint()`. - /// Return the error if any and a new `newHandle` of this sound + @override ({PlayerErrors error, SoundHandle newHandle}) play( SoundHash soundHash, { double volume = 1, @@ -571,9 +453,7 @@ class FlutterSoLoudFfi { int Function(int, double, double, int, int, double, ffi.Pointer)>(); - /// Stop already loaded sound identified by [handle] and clear it. - /// - /// [handle] + @override void stop(SoundHandle handle) { return _stop(handle.id); } @@ -582,10 +462,7 @@ class FlutterSoLoudFfi { _lookup>('stop'); late final _stop = _stopPtr.asFunction(); - /// Stop all handles of the already loaded sound identified - /// by [soundHash] and dispose it. - /// - /// [soundHash] + @override void disposeSound(SoundHash soundHash) { return _disposeSound(soundHash.hash); } @@ -596,7 +473,7 @@ class FlutterSoLoudFfi { ); late final _disposeSound = _disposeSoundPtr.asFunction(); - /// Dispose all sounds already loaded + @override void disposeAllSound() { return _disposeAllSound(); } @@ -606,10 +483,7 @@ class FlutterSoLoudFfi { late final _disposeAllSound = _disposeAllSoundPtr.asFunction(); - /// Query whether a sound is set to loop. - /// - /// [handle] - /// Returns true if flagged for looping. + @override bool getLooping(SoundHandle handle) { return _getLooping(handle.id) == 1; } @@ -619,11 +493,7 @@ class FlutterSoLoudFfi { 'getLooping'); late final _getLooping = _getLoopingPtr.asFunction(); - /// This function can be used to set a sample to play on repeat, - /// instead of just playing once - /// - /// [handle] - /// [enable] + @override void setLooping(SoundHandle handle, bool enable) { return _setLooping(handle.id, enable ? 1 : 0); } @@ -634,10 +504,7 @@ class FlutterSoLoudFfi { ); late final _setLooping = _setLoopingPtr.asFunction(); - /// Get sound loop point value. - /// - /// [handle] - /// Returns the duration. + @override Duration getLoopPoint(SoundHandle handle) { return _getLoopPoint(handle.id).toDuration(); } @@ -648,10 +515,7 @@ class FlutterSoLoudFfi { late final _getLoopPoint = _getLoopPointPtr.asFunction(); - /// Set sound loop point value. - /// - /// [handle] - /// [timestamp] + @override void setLoopPoint(SoundHandle handle, Duration timestamp) { _setLoopPoint(handle.id, timestamp.toDouble()); } @@ -662,11 +526,7 @@ class FlutterSoLoudFfi { late final _setLoopPoint = _setLoopPointPtr.asFunction(); - // TODO(marco): implement Soloud.getLoopCount() also? - - /// Enable or disable visualization - /// - /// [enabled] enable or disable it + @override void setVisualizationEnabled(bool enabled) { return _setVisualizationEnabled( enabled ? 1 : 0, @@ -680,9 +540,7 @@ class FlutterSoLoudFfi { late final _setVisualizationEnabled = _setVisualizationEnabledPtr.asFunction(); - /// Get visualization state - /// - /// Return true if enabled + @override bool getVisualizationEnabled() { return _getVisualizationEnabled() == 1; } @@ -693,46 +551,29 @@ class FlutterSoLoudFfi { late final _getVisualizationEnabled = _getVisualizationEnabledPtr.asFunction(); - /// Returns valid data only if VisualizationEnabled is true - /// - /// [fft] - /// Return a 256 float array containing FFT data. - void getFft(ffi.Pointer fft) { - return _getFft(fft); + @override + void getFft(AudioData fft) { + return _getFft(fft.ctrl.samplesWave); } - late final _getFftPtr = - _lookup)>>( - 'getFft', - ); - late final _getFft = - _getFftPtr.asFunction)>(); + late final _getFftPtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>)>>('getFft'); + late final _getFft = _getFftPtr + .asFunction>)>(); - /// Returns valid data only if VisualizationEnabled is true - /// - /// fft - /// Return a 256 float array containing wave data. - void getWave(ffi.Pointer wave) { - return _getWave(wave); + @override + void getWave(AudioData wave) { + return _getWave(wave.ctrl.samplesWave); } - late final _getWavePtr = - _lookup)>>( - 'getWave', - ); - late final _getWave = - _getWavePtr.asFunction)>(); + late final _getWavePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function(ffi.Pointer>)>>('getWave'); + late final _getWave = _getWavePtr + .asFunction>)>(); - /// Smooth FFT data. - /// When new data is read and the values are decreasing, the new value will be - /// decreased with an amplitude between the old and the new value. - /// This will result on a less shaky visualization. - /// - /// [smooth] must be in the [0.0 ~ 1.0] range. - /// 0 = no smooth - /// 1 = full smooth - /// the new value is calculated with: - /// newFreq = smooth * oldFreq + (1 - smooth) * newFreq + @override void setFftSmoothing(double smooth) { return _setFftSmoothing(smooth); } @@ -744,13 +585,9 @@ class FlutterSoLoudFfi { late final _setFftSmoothing = _setFftSmoothingPtr.asFunction(); - /// Return in [samples] a 512 float array. - /// The first 256 floats represent the FFT frequencies data [>=0.0]. - /// The other 256 floats represent the wave data (amplitude) [-1.0~1.0]. - /// - /// [samples] should be allocated and freed in dart side - void getAudioTexture(ffi.Pointer samples) { - return _getAudioTexture(samples); + @override + void getAudioTexture(AudioData samples) { + return _getAudioTexture(samples.ctrl.samples1D); } late final _getAudioTexturePtr = @@ -760,16 +597,9 @@ class FlutterSoLoudFfi { late final _getAudioTexture = _getAudioTexturePtr.asFunction)>(); - /// Return a floats matrix of 256x512 - /// Every row are composed of 256 FFT values plus 256 of wave data - /// Every time is called, a new row is stored in the - /// first row and all the previous rows are shifted - /// up (the last one will be lost). - /// - /// [samples] - PlayerErrors getAudioTexture2D(ffi.Pointer> samples) { - if (samples == ffi.nullptr) return PlayerErrors.nullPointer; - final ret = _getAudioTexture2D(samples); + @override + PlayerErrors getAudioTexture2D(AudioData samples) { + final ret = _getAudioTexture2D(samples.ctrl.samples2D); return PlayerErrors.values[ret]; } @@ -781,10 +611,18 @@ class FlutterSoLoudFfi { late final _getAudioTexture2D = _getAudioTexture2DPtr .asFunction>)>(); - /// Get the sound length. - /// - /// [soundHash] the sound hash - /// Returns sound length in seconds + @override + double getTextureValue(int row, int column) { + return _getTextureValue(row, column); + } + + late final _getTextureValuePtr = + _lookup>( + 'getTextureValue'); + late final _getTextureValue = + _getTextureValuePtr.asFunction(); + + @override Duration getLength(SoundHash soundHash) { return _getLength(soundHash.hash).toDuration(); } @@ -795,22 +633,7 @@ class FlutterSoLoudFfi { ); late final _getLength = _getLengthPtr.asFunction(); - /// Seek playing in [time] seconds - /// [time] - /// [handle] the sound handle - /// Returns [PlayerErrors.noError] if success - /// - /// NOTE: when seeking an MP3 file loaded using `mode`=`LoadMode.disk` the - /// seek operation is performed but there will be delays. This occurs because - /// the MP3 codec must compute each frame length to gain a new position. - /// The problem is explained in souloud_wavstream.cpp - /// in `WavStreamInstance::seek` function. - /// - /// This mode is useful ie for background music, not for a music player - /// where a seek slider for MP3s is a must. - /// If you need to seek MP3s without lags, please, use - /// `mode`=`LoadMode.memory` instead or other supported audio formats! - /// + @override int seek(SoundHandle handle, Duration time) { return _seek(handle.id, time.toDouble()); } @@ -821,10 +644,7 @@ class FlutterSoLoudFfi { ); late final _seek = _seekPtr.asFunction(); - /// Get current sound position in seconds - /// - /// [handle] the sound handle - /// Returns time + @override Duration getPosition(SoundHandle handle) { return _getPosition(handle.id).toDuration(); } @@ -835,9 +655,7 @@ class FlutterSoLoudFfi { ); late final _getPosition = _getPositionPtr.asFunction(); - /// Get current Global volume - /// - /// Returns the volume + @override double getGlobalVolume() { return _getGlobalVolume(); } @@ -847,9 +665,7 @@ class FlutterSoLoudFfi { late final _getGlobalVolume = _getGlobalVolumePtr.asFunction(); - /// Set current Global volume - /// - /// Returns [PlayerErrors.noError] if success + @override int setGlobalVolume(double volume) { return _setGlobalVolume(volume); } @@ -860,9 +676,7 @@ class FlutterSoLoudFfi { late final _setGlobalVolume = _setGlobalVolumePtr.asFunction(); - /// Get current [handle] volume - /// - /// Returns the volume + @override double getVolume(SoundHandle handle) { return _getVolume(handle.id); } @@ -872,9 +686,7 @@ class FlutterSoLoudFfi { 'getVolume'); late final _getVolume = _getVolumePtr.asFunction(); - /// Set current [handle] volume - /// - /// Returns [PlayerErrors.noError] if success + @override int setVolume(SoundHandle handle, double volume) { return _setVolume(handle.id, volume); } @@ -889,10 +701,11 @@ class FlutterSoLoudFfi { /// [handle] the sound handle. /// Returns the range of the pan values is -1 to 1, where -1 is left, 0 is /// middle and and 1 is right. - double getPan(int handle) { + @override + double getPan(SoundHandle handle) { // Note that because of the float<=>double conversion precision error // (SoLoud lib uses floats), the returned value is not precise. - return _getPan(handle); + return _getPan(handle.id); } late final _getPanPtr = @@ -905,8 +718,9 @@ class FlutterSoLoudFfi { /// [handle] the sound handle. /// [pan] the range of the pan values is -1 to 1, where -1 is left, 0 is /// middle and and 1 is right. - void setPan(int handle, double pan) { - return _setPan(handle, pan); + @override + void setPan(SoundHandle handle, double pan) { + return _setPan(handle.id, pan); } late final _setPanPtr = _lookup< @@ -920,8 +734,9 @@ class FlutterSoLoudFfi { /// [handle] the sound handle. /// [panLeft] value for the left pan. /// [panRight] value for the right pan. - void setPanAbsolute(int handle, double panLeft, double panRight) { - return _setPanAbsolute(handle, panLeft, panRight); + @override + void setPanAbsolute(SoundHandle handle, double panLeft, double panRight) { + return _setPanAbsolute(handle.id, panLeft, panRight); } late final _setPanAbsolutePtr = _lookup< @@ -935,6 +750,7 @@ class FlutterSoLoudFfi { /// /// [handle] handle to check /// Return true if it still exists + @override bool getIsValidVoiceHandle(SoundHandle handle) { return _getIsValidVoiceHandle(handle.id) == 1; } @@ -946,7 +762,7 @@ class FlutterSoLoudFfi { late final _getIsValidVoiceHandle = _getIsValidVoiceHandlePtr.asFunction(); - /// Returns the number of concurrent sounds that are playing at the moment. + @override int getActiveVoiceCount() { return _getActiveVoiceCount(); } @@ -957,8 +773,7 @@ class FlutterSoLoudFfi { late final _getActiveVoiceCount = _getActiveVoiceCountPtr.asFunction(); - /// Returns the number of concurrent sounds that are playing a - /// specific audio source. + @override int countAudioSource(SoundHash soundHash) { return _countAudioSource(soundHash.hash); } @@ -969,7 +784,7 @@ class FlutterSoLoudFfi { late final _countAudioSource = _countAudioSourcePtr.asFunction(); - /// Returns the number of voices the application has told SoLoud to play. + @override int getVoiceCount() { return _getVoiceCount(); } @@ -978,7 +793,7 @@ class FlutterSoLoudFfi { _lookup>('getVoiceCount'); late final _getVoiceCount = _getVoiceCountPtr.asFunction(); - /// Get a sound's protection state. + @override bool getProtectVoice(SoundHandle handle) { return _getProtectVoice(handle.id) == 1; } @@ -989,16 +804,7 @@ class FlutterSoLoudFfi { late final _getProtectVoice = _getProtectVoicePtr.asFunction(); - /// Set a sound's protection state. - /// - /// Normally, if you try to play more sounds than there are voices, - /// SoLoud will kill off the oldest playing sound to make room. - /// This will most likely be your background music. This can be worked - /// around by protecting the sound. - /// If all voices are protected, the result will be undefined. - /// - /// [handle] handle to check. - /// [protect] whether to protect or not. + @override void setProtectVoice(SoundHandle handle, bool protect) { return _setProtectVoice(handle.id, protect ? 1 : 0); } @@ -1009,7 +815,7 @@ class FlutterSoLoudFfi { late final _setProtectVoice = _setProtectVoicePtr.asFunction(); - /// Get the current maximum active voice count. + @override int getMaxActiveVoiceCount() { return _getMaxActiveVoiceCount(); } @@ -1020,18 +826,7 @@ class FlutterSoLoudFfi { late final _getMaxActiveVoiceCount = _getMaxActiveVoiceCountPtr.asFunction(); - /// Set the current maximum active voice count. - /// If voice count is higher than the maximum active voice count, - /// SoLoud will pick the ones with the highest volume to actually play. - /// [maxVoiceCount] the max concurrent sounds that can be played. - /// - /// NOTE: The number of concurrent voices is limited, as having unlimited - /// voices would cause performance issues, as well as lead to unnecessary - /// clipping. The default number of concurrent voices is 16, but this can be - /// adjusted at runtime. The hard maximum number is 4095, but if more are - /// required, SoLoud can be modified to support more. But seriously, if you - /// need more than 4095 sounds at once, you're probably going to make - /// some serious changes in any case. + @override void setMaxActiveVoiceCount(int maxVoiceCount) { return _setMaxActiveVoiceCount(maxVoiceCount); } @@ -1046,8 +841,7 @@ class FlutterSoLoudFfi { /// faders ///////////////////////////////////////// - /// Smoothly change the global volume over specified [duration]. - /// + @override int fadeGlobalVolume(double to, Duration duration) { return _fadeGlobalVolume(to, duration.toDouble()); } @@ -1058,8 +852,7 @@ class FlutterSoLoudFfi { late final _fadeGlobalVolume = _fadeGlobalVolumePtr.asFunction(); - /// Smoothly change a channel's volume over specified [duration]. - /// + @override int fadeVolume(SoundHandle handle, double to, Duration duration) { return _fadeVolume(handle.id, to, duration.toDouble()); } @@ -1071,8 +864,7 @@ class FlutterSoLoudFfi { late final _fadeVolume = _fadeVolumePtr.asFunction(); - /// Smoothly change a channel's pan setting over specified [duration]. - /// + @override int fadePan(SoundHandle handle, double to, Duration duration) { return _fadePan(handle.id, to, duration.toDouble()); } @@ -1084,8 +876,7 @@ class FlutterSoLoudFfi { late final _fadePan = _fadePanPtr.asFunction(); - /// Smoothly change a channel's relative play speed over specified time. - /// + @override int fadeRelativePlaySpeed(SoundHandle handle, double to, Duration time) { return _fadeRelativePlaySpeed(handle.id, to, time.toDouble()); } @@ -1097,8 +888,7 @@ class FlutterSoLoudFfi { late final _fadeRelativePlaySpeed = _fadeRelativePlaySpeedPtr.asFunction(); - /// After specified [duration], pause the channel. - /// + @override int schedulePause(SoundHandle handle, Duration duration) { return _schedulePause(handle.id, duration.toDouble()); } @@ -1109,8 +899,7 @@ class FlutterSoLoudFfi { late final _schedulePause = _schedulePausePtr.asFunction(); - /// After specified time, stop the channel. - /// + @override int scheduleStop(SoundHandle handle, Duration duration) { return _scheduleStop(handle.id, duration.toDouble()); } @@ -1121,8 +910,7 @@ class FlutterSoLoudFfi { late final _scheduleStop = _scheduleStopPtr.asFunction(); - /// Set fader to oscillate the volume at specified frequency. - /// + @override int oscillateVolume( SoundHandle handle, double from, double to, Duration time) { return _oscillateVolume(handle.id, from, to, time.toDouble()); @@ -1135,8 +923,7 @@ class FlutterSoLoudFfi { late final _oscillateVolume = _oscillateVolumePtr .asFunction(); - /// Set fader to oscillate the panning at specified frequency. - /// + @override int oscillatePan(SoundHandle handle, double from, double to, Duration time) { return _oscillatePan(handle.id, from, to, time.toDouble()); } @@ -1148,8 +935,7 @@ class FlutterSoLoudFfi { late final _oscillatePan = _oscillatePanPtr.asFunction(); - /// Set fader to oscillate the relative play speed at specified frequency. - /// + @override int oscillateRelativePlaySpeed( SoundHandle handle, double from, double to, Duration time) { return _oscillateRelativePlaySpeed(handle.id, from, to, time.toDouble()); @@ -1162,8 +948,7 @@ class FlutterSoLoudFfi { late final _oscillateRelativePlaySpeed = _oscillateRelativePlaySpeedPtr .asFunction(); - /// Set fader to oscillate the global volume at specified frequency. - /// + @override int oscillateGlobalVolume(double from, double to, Duration time) { return _oscillateGlobalVolume(from, to, time.toDouble()); } @@ -1175,20 +960,15 @@ class FlutterSoLoudFfi { late final _oscillateGlobalVolume = _oscillateGlobalVolumePtr .asFunction(); - ///////////////////////////////////////// - /// Filters - ///////////////////////////////////////// + // /////////////////////////////////////// + // Filters + // /////////////////////////////////////// - /// Check if the given filter is active or not. - /// - /// [filterType] filter to check - /// Returns [PlayerErrors.noError] if no errors and the index of - /// the given filter (-1 if the filter is not active) - /// - ({PlayerErrors error, int index}) isFilterActive(int filterType) { + @override + ({PlayerErrors error, int index}) isFilterActive(FilterType filterType) { // ignore: omit_local_variable_types final ffi.Pointer id = calloc(ffi.sizeOf()); - final e = _isFilterActive(filterType, id); + final e = _isFilterActive(filterType.index, id); final ret = (error: PlayerErrors.values[e], index: id.value); calloc.free(id); return ret; @@ -1201,13 +981,9 @@ class FlutterSoLoudFfi { late final _isFilterActive = _isFilterActivePtr.asFunction)>(); - /// Get parameters names of the given filter. - /// - /// [filterType] filter to get param names - /// Returns [PlayerErrors.noError] if no errors and the list of param names - /// + @override ({PlayerErrors error, List names}) getFilterParamNames( - int filterType) { + FilterType filterType) { // ignore: omit_local_variable_types final ffi.Pointer paramsCount = calloc(ffi.sizeOf()); // ignore: omit_local_variable_types @@ -1218,7 +994,7 @@ class FlutterSoLoudFfi { 'names: ${names.address.toRadixString(16)}'); final e = _getFilterParamNames( - filterType, + filterType.index, paramsCount, names, ); @@ -1246,19 +1022,9 @@ class FlutterSoLoudFfi { int Function( int, ffi.Pointer, ffi.Pointer>)>(); - /// Add the filter [filterType]. - /// - /// [filterType] filter to add. - /// Returns: - /// [PlayerErrors.noError] if no errors - /// [PlayerErrors.filterNotFound] if the [filterType] does not exits - /// [PlayerErrors.filterAlreadyAdded] when trying to add an already - /// added filter - /// [PlayerErrors.maxNumberOfFiltersReached] when the maximum number of - /// filters has been reached (default is 8) - /// - PlayerErrors addGlobalFilter(int filterType) { - final e = _addGlobalFilter(filterType); + @override + PlayerErrors addGlobalFilter(FilterType filterType) { + final e = _addGlobalFilter(filterType.index); return PlayerErrors.values[e]; } @@ -1268,13 +1034,9 @@ class FlutterSoLoudFfi { late final _addGlobalFilter = _addGlobalFilterPtr.asFunction(); - /// Remove the filter [filterType]. - /// - /// [filterType] filter to remove - /// Returns [PlayerErrors.noError] if no errors - /// - int removeGlobalFilter(int filterType) { - return _removeGlobalFilter(filterType); + @override + int removeGlobalFilter(FilterType filterType) { + return _removeGlobalFilter(filterType.index); } late final _removeGlobalFilterPtr = @@ -1283,14 +1045,9 @@ class FlutterSoLoudFfi { late final _removeGlobalFilter = _removeGlobalFilterPtr.asFunction(); - /// Set the effect parameter with id [attributeId] - /// of [filterType] with [value] value. - /// - /// [filterType] filter to modify a param - /// Returns [PlayerErrors.noError] if no errors - /// - int setFilterParams(int filterType, int attributeId, double value) { - return _setFxParams(filterType, attributeId, value); + @override + int setFilterParams(FilterType filterType, int attributeId, double value) { + return _setFxParams(filterType.index, attributeId, value); } late final _setFxParamsPtr = _lookup< @@ -1299,13 +1056,9 @@ class FlutterSoLoudFfi { late final _setFxParams = _setFxParamsPtr.asFunction(); - /// Get the effect parameter with id [attributeId] of [filterType]. - /// - /// [filterType] filter to modify a param - /// Returns the value of param - /// - double getFilterParams(int filterType, int attributeId) { - return _getFxParams(filterType, attributeId); + @override + double getFilterParams(FilterType filterType, int attributeId) { + return _getFxParams(filterType.index, attributeId); } late final _getFxParamsPtr = @@ -1318,16 +1071,7 @@ class FlutterSoLoudFfi { /// 3D audio methods ///////////////////////////////////////// - /// play3d() is the 3d version of the play() call - /// - /// [posX], [posY], [posZ] are the audio source position coordinates. - /// [velX], [velY], [velZ] are the audio source velocity. - /// [looping] whether to start the sound in looping state. - /// [loopingStartAt] If looping is enabled, the loop point is, by default, - /// the start of the stream. The loop start point can be set with this - /// parameter, and current loop point can be queried with `getLoopingPoint()` - /// and changed by `setLoopingPoint()`. - /// Returns the handle of the sound, 0 if error + @override ({PlayerErrors error, SoundHandle newHandle}) play3d( SoundHash soundHash, double posX, @@ -1382,12 +1126,7 @@ class FlutterSoLoudFfi { int Function(int, double, double, double, double, double, double, double, int, int, double, ffi.Pointer)>(); - /// Since SoLoud has no knowledge of the scale of your coordinates, - /// you may need to adjust the speed of sound for these effects - /// to work correctly. The default value is 343, which assumes - /// that your world coordinates are in meters (where 1 unit is 1 meter), - /// and that the environment is dry air at around 20 degrees Celsius. - /// + @override void set3dSoundSpeed(double speed) { return _set3dSoundSpeed(speed); } @@ -1399,8 +1138,7 @@ class FlutterSoLoudFfi { late final _set3dSoundSpeed = _set3dSoundSpeedPtr.asFunction(); - /// Get the sound speed. - /// + @override double get3dSoundSpeed() { return _get3dSoundSpeed(); } @@ -1410,9 +1148,7 @@ class FlutterSoLoudFfi { late final _get3dSoundSpeed = _get3dSoundSpeedPtr.asFunction(); - /// You can set the position, at-vector, up-vector and velocity - /// parameters of the 3d audio listener with one call - /// + @override void set3dListenerParameters( double posX, double posY, @@ -1475,8 +1211,7 @@ class FlutterSoLoudFfi { double, )>(); - /// You can set the position parameter of the 3d audio listener - /// + @override void set3dListenerPosition(double posX, double posY, double posZ) { return _set3dListenerPosition(posX, posY, posZ); } @@ -1491,8 +1226,7 @@ class FlutterSoLoudFfi { late final _set3dListenerPosition = _set3dListenerPositionPtr .asFunction(); - /// You can set the "at" vector parameter of the 3d audio listener. - /// + @override void set3dListenerAt(double atX, double atY, double atZ) { return _set3dListenerAt(atX, atY, atZ); } @@ -1507,8 +1241,7 @@ class FlutterSoLoudFfi { late final _set3dListenerAt = _set3dListenerAtPtr.asFunction(); - /// You can set the "up" vector parameter of the 3d audio listener. - /// + @override void set3dListenerUp(double upX, double upY, double upZ) { return _set3dListenerUp(upX, upY, upZ); } @@ -1523,8 +1256,7 @@ class FlutterSoLoudFfi { late final _set3dListenerUp = _set3dListenerUpPtr.asFunction(); - /// You can set the listener's velocity vector parameter. - /// + @override void set3dListenerVelocity( double velocityX, double velocityY, @@ -1543,9 +1275,7 @@ class FlutterSoLoudFfi { late final _set3dListenerVelocity = _set3dListenerVelocityPtr .asFunction(); - /// You can set the position and velocity parameters of a live - /// 3d audio source with one call. - /// + @override void set3dSourceParameters( SoundHandle handle, double posX, @@ -1580,8 +1310,7 @@ class FlutterSoLoudFfi { late final _set3dSourceParameters = _set3dSourceParametersPtr.asFunction< void Function(int, double, double, double, double, double, double)>(); - /// You can set the position parameters of a live 3d audio source - /// + @override void set3dSourcePosition( SoundHandle handle, double posX, double posY, double posZ) { return _set3dSourcePosition(handle.id, posX, posY, posZ); @@ -1598,8 +1327,7 @@ class FlutterSoLoudFfi { late final _set3dSourcePosition = _set3dSourcePositionPtr .asFunction(); - /// You can set the velocity parameters of a live 3d audio source - /// + @override void set3dSourceVelocity( SoundHandle handle, double velocityX, @@ -1620,9 +1348,7 @@ class FlutterSoLoudFfi { late final _set3dSourceVelocity = _set3dSourceVelocityPtr .asFunction(); - /// You can set the minimum and maximum distance parameters - /// of a live 3d audio source - /// + @override void set3dSourceMinMaxDistance( SoundHandle handle, double minDistance, @@ -1641,16 +1367,7 @@ class FlutterSoLoudFfi { late final _set3dSourceMinMaxDistance = _set3dSourceMinMaxDistancePtr .asFunction(); - /// You can change the attenuation model and rolloff factor parameters of - /// a live 3d audio source. - /// - /// NO_ATTENUATION No attenuation - /// INVERSE_DISTANCE Inverse distance attenuation model - /// LINEAR_DISTANCE Linear distance attenuation model - /// EXPONENTIAL_DISTANCE Exponential distance attenuation model - /// - /// see https://solhsa.com/soloud/concepts3d.html - /// + @override void set3dSourceAttenuation( SoundHandle handle, int attenuationModel, @@ -1673,8 +1390,7 @@ class FlutterSoLoudFfi { late final _set3dSourceAttenuation = _set3dSourceAttenuationPtr.asFunction(); - /// You can change the doppler factor of a live 3d audio source - /// + @override void set3dSourceDopplerFactor(SoundHandle handle, double dopplerFactor) { return _set3dSourceDopplerFactor(handle.id, dopplerFactor); } @@ -1685,29 +1401,4 @@ class FlutterSoLoudFfi { ); late final _set3dSourceDopplerFactor = _set3dSourceDopplerFactorPtr.asFunction(); - - /// internal test. Does nothing now - /// - void test() { - return _test(); - } - - late final _testPtr = - _lookup>('test'); - late final _test = _testPtr.asFunction(); -} - -/// Used for easier conversion from [double] to [Duration]. -extension _DoubleToDuration on double { - Duration toDuration() { - return Duration( - microseconds: (this * Duration.microsecondsPerSecond).round()); - } -} - -/// Used for easier conversion from [Duration] to [double]. -extension _DurationToDouble on Duration { - double toDouble() { - return inMicroseconds / Duration.microsecondsPerSecond; - } } diff --git a/lib/src/bindings/bindings_player_web.dart b/lib/src/bindings/bindings_player_web.dart new file mode 100644 index 0000000..1bfffa2 --- /dev/null +++ b/lib/src/bindings/bindings_player_web.dart @@ -0,0 +1,724 @@ +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/bindings_player.dart'; +import 'package:flutter_soloud/src/bindings/js_extension.dart'; +import 'package:flutter_soloud/src/enums.dart'; +import 'package:flutter_soloud/src/filter_params.dart'; +import 'package:flutter_soloud/src/sound_handle.dart'; +import 'package:flutter_soloud/src/sound_hash.dart'; +import 'package:flutter_soloud/src/worker/worker.dart'; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; + +/// https://kapadia.github.io/emscripten/2013/09/13/emscripten-pointers-and-pointers.html +/// https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#access-memory-from-javascript + +/// https://github.com/isar/isar/blob/main/packages/isar/lib/src/web/web.dart +/// chromium --disable-web-security --disable-gpu --user-data-dir=~/chromeTemp +/// +/// Call Dart method from JS in Flutter Web +/// https://stackoverflow.com/questions/65423861/call-dart-method-from-js-in-flutter-web + +/// JS/WASM bindings to SoLoud +@internal +class FlutterSoLoudWeb extends FlutterSoLoud { + static final Logger _log = Logger('flutter_soloud.FlutterSoLoudFfi'); + + WorkerController? workerController; + + /// Create the worker in the WASM Module and listen for events coming + /// from `web/worker.dart.js` + @override + void setDartEventCallbacks() { + // This calls the native WASM `createWorkerInWasm()` in `bindings.cpp`. + // The latter creates a web Worker using `EM_ASM` inlining JS code to + // create the worker in the WASM `Module`. + wasmCreateWorkerInWasm(); + + // Here the `Module.wasmModule` binded to a local [WorkerController] + // is used in the main isolate to listen for events coming from native. + // From native the events can be sent from the main thread and even from + // other threads like the audio thread. + workerController = WorkerController(); + workerController!.setWasmWorker(wasmWorker); + workerController!.onReceive().listen( + (event) { + /// The [event] coming from `web/worker.dart.js` is of String type. + /// Only `voiceEndedCallback` event in web for now. + switch (event) { + case String(): + final decodedMap = jsonDecode(event) as Map; + if (decodedMap['message'] == 'voiceEndedCallback') { + _log.finest( + () => 'VOICE ENDED EVENT handle: ${decodedMap['value']}\n', + ); + voiceEndedEventController.add(decodedMap['value'] as int); + } + } + }, + ); + } + + /// If we will need to send messages to the native. Not used now. + void sendMessageToWasmWorker(String message, int value) { + final messagePtr = wasmMalloc(message.length); + for (var i = 0; i < message.length; i++) { + wasmSetValue(messagePtr + i, message.codeUnits[i], 'i8'); + } + wasmSendToWorker(messagePtr, value); + wasmFree(messagePtr); + } + + @override + PlayerErrors initEngine() { + return PlayerErrors.values[wasmInitEngine()]; + } + + @override + void deinit() => wasmDeinit(); + + @override + bool isInited() => wasmIsInited() == 1; + + @override + ({PlayerErrors error, SoundHash soundHash}) loadFile( + String completeFileName, + LoadMode mode, + ) { + throw UnimplementedError('[loadFile] in not supported on the web platfom! ' + 'Please use [loadMem].'); + } + + @override + ({PlayerErrors error, SoundHash soundHash}) loadMem( + String uniqueName, + Uint8List buffer, + LoadMode mode, + ) { + final hashPtr = wasmMalloc(4); // 4 bytes for an int + final bytesPtr = wasmMalloc(buffer.length); + final pathPtr = wasmMalloc(uniqueName.length); + // Is there a way to speed up this array copy? + for (var i = 0; i < buffer.length; i++) { + wasmSetValue(bytesPtr + i, buffer[i], 'i8'); + } + for (var i = 0; i < uniqueName.length; i++) { + wasmSetValue(pathPtr + i, uniqueName.codeUnits[i], 'i8'); + } + + final result = wasmLoadMem( + pathPtr, + bytesPtr, + buffer.length, + mode == LoadMode.memory ? 1 : 0, + hashPtr, + ); + + /// "*" means unsigned int 32 + final hash = wasmGetI32Value(hashPtr, '*'); + final soundHash = SoundHash(hash); + final ret = (error: PlayerErrors.values[result], soundHash: soundHash); + + wasmFree(hashPtr); + wasmFree(bytesPtr); + wasmFree(pathPtr); + + return ret; + } + + @override + ({PlayerErrors error, SoundHash soundHash}) loadWaveform( + WaveForm waveform, + bool superWave, + double scale, + double detune, + ) { + final hashPtr = wasmMalloc(4); // 4 bytes for an int + final result = wasmLoadWaveform( + waveform.index, + superWave, + scale, + detune, + hashPtr, + ); + + /// "*" means unsigned int 32 + final hash = wasmGetI32Value(hashPtr, '*'); + final soundHash = SoundHash(hash); + final ret = (error: PlayerErrors.values[result], soundHash: soundHash); + wasmFree(hashPtr); + + return ret; + } + + @override + void setWaveformScale(SoundHash hash, double newScale) { + return wasmSetWaveformScale(hash.hash, newScale); + } + + @override + void setWaveformDetune(SoundHash hash, double newDetune) { + return wasmSetWaveformDetune(hash.hash, newDetune); + } + + @override + void setWaveformFreq(SoundHash hash, double newFreq) { + return wasmSetWaveformFreq(hash.hash, newFreq); + } + + @override + void setWaveformSuperWave(SoundHash hash, int superwave) { + return wasmSetSuperWave(hash.hash, superwave); + } + + @override + void setWaveform(SoundHash hash, WaveForm newWaveform) { + return wasmSetWaveform(hash.hash, newWaveform.index); + } + + @override + ({PlayerErrors error, SoundHandle handle}) speechText(String textToSpeech) { + final handlePtr = wasmMalloc(4); // 4 bytes for an int + final textToSpeechPtr = wasmMalloc(textToSpeech.length); + final result = wasmSpeechText( + textToSpeechPtr, + handlePtr, + ); + + /// "*" means unsigned int 32 + final newHandle = wasmGetI32Value(handlePtr, '*'); + final ret = ( + error: PlayerErrors.values[result], + handle: SoundHandle(newHandle), + ); + wasmFree(textToSpeechPtr); + wasmFree(handlePtr); + + return ret; + } + + @override + void pauseSwitch(SoundHandle handle) { + return wasmPauseSwitch(handle.id); + } + + @override + void setPause(SoundHandle handle, int pause) { + return wasmSetPause(handle.id, pause); + } + + @override + bool getPause(SoundHandle handle) { + return wasmGetPause(handle.id) == 1; + } + + @override + void setRelativePlaySpeed(SoundHandle handle, double speed) { + return wasmSetRelativePlaySpeed(handle.id, speed); + } + + @override + double getRelativePlaySpeed(SoundHandle handle) { + return wasmGetRelativePlaySpeed(handle.id); + } + + @override + ({PlayerErrors error, SoundHandle newHandle}) play( + SoundHash soundHash, { + double volume = 1, + double pan = 0, + bool paused = false, + bool looping = false, + Duration loopingStartAt = Duration.zero, + }) { + final handlePtr = wasmMalloc(4); // 4 bytes for an int + final result = wasmPlay( + soundHash.hash, + volume, + pan, + paused, + looping, + loopingStartAt.toDouble(), + handlePtr, + ); + + /// "*" means unsigned int 32 + final newHandle = wasmGetI32Value(handlePtr, '*'); + final ret = + (error: PlayerErrors.values[result], newHandle: SoundHandle(newHandle)); + wasmFree(handlePtr); + + return ret; + } + + @override + void stop(SoundHandle handle) { + return wasmStop(handle.id); + } + + @override + void disposeSound(SoundHash soundHash) { + return wasmDisposeSound(soundHash.hash); + } + + @override + void disposeAllSound() { + return wasmDisposeAllSound(); + } + + @override + bool getLooping(SoundHandle handle) { + return wasmGetLooping(handle.id) == 1; + } + + @override + void setLooping(SoundHandle handle, bool enable) { + return wasmSetLooping(handle.id, enable ? 1 : 0); + } + + @override + Duration getLoopPoint(SoundHandle handle) { + return wasmGetLoopPoint(handle.id).toDuration(); + } + + @override + void setLoopPoint(SoundHandle handle, Duration timestamp) { + wasmSetLoopPoint(handle.id, timestamp.toDouble()); + } + + @override + void setVisualizationEnabled(bool enabled) { + wasmSetVisualizationEnabled(enabled ? 1 : 0); + } + + @override + bool getVisualizationEnabled() { + return wasmGetVisualizationEnabled() == 1; + } + + @override + void getFft(AudioData fft) { + wasmGetWave(fft.ctrl.samplesPtr); + } + + @override + void getWave(AudioData wave) { + wasmGetWave(wave.ctrl.samplesPtr); + } + + @override + void setFftSmoothing(double smooth) { + wasmSetFftSmoothing(smooth); + } + + @override + void getAudioTexture(AudioData samples) { + wasmGetAudioTexture(samples.ctrl.samplesPtr); + } + + @override + PlayerErrors getAudioTexture2D(AudioData samples) { + final e = wasmGetAudioTexture2D(samples.ctrl.samplesPtr); + return PlayerErrors.values[e]; + } + + @override + double getTextureValue(int row, int column) { + final e = wasmGetTextureValue(row, column); + return e; + } + + @override + Duration getLength(SoundHash soundHash) { + return wasmGetLength(soundHash.hash).toDuration(); + } + + @override + int seek(SoundHandle handle, Duration time) { + return wasmSeek(handle.id, time.toDouble()); + } + + @override + Duration getPosition(SoundHandle handle) { + return wasmGetPosition(handle.id).toDuration(); + } + + @override + double getGlobalVolume() { + return wasmGetGlobalVolume(); + } + + @override + int setGlobalVolume(double volume) { + return wasmSetGlobalVolume(volume); + } + + @override + double getVolume(SoundHandle handle) { + return wasmGetVolume(handle.id); + } + + @override + int setVolume(SoundHandle handle, double volume) { + return wasmSetVolume(handle.id, volume); + } + + @override + double getPan(SoundHandle handle) { + return wasmGetPan(handle.id); + } + + @override + void setPan(SoundHandle handle, double pan) { + return wasmSetPan(handle.id, pan); + } + + @override + void setPanAbsolute(SoundHandle handle, double panLeft, double panRight) { + return wasmSetPanAbsolute(handle.id, panLeft, panRight); + } + + @override + bool getIsValidVoiceHandle(SoundHandle handle) { + return wasmGetIsValidVoiceHandle(handle.id) == 1; + } + + @override + int getActiveVoiceCount() { + return wasmGetActiveVoiceCount(); + } + + @override + int countAudioSource(SoundHash soundHash) { + return wasmCountAudioSource(soundHash.hash); + } + + @override + int getVoiceCount() { + return wasmGetVoiceCount(); + } + + @override + bool getProtectVoice(SoundHandle handle) { + return wasmGetProtectVoice(handle.id) == 1; + } + + @override + void setProtectVoice(SoundHandle handle, bool protect) { + return wasmSetProtectVoice(handle.id, protect ? 1 : 0); + } + + @override + int getMaxActiveVoiceCount() { + return wasmGetMaxActiveVoiceCount(); + } + + @override + void setMaxActiveVoiceCount(int maxVoiceCount) { + return wasmSetMaxActiveVoiceCount(maxVoiceCount); + } + + // /////////////////////////////////////// + // faders + // /////////////////////////////////////// + + @override + int fadeGlobalVolume(double to, Duration duration) { + return wasmFadeGlobalVolume(to, duration.toDouble()); + } + + @override + int fadeVolume(SoundHandle handle, double to, Duration duration) { + return wasmFadeVolume(handle.id, to, duration.toDouble()); + } + + @override + int fadePan(SoundHandle handle, double to, Duration duration) { + return wasmFadePan(handle.id, to, duration.toDouble()); + } + + @override + int fadeRelativePlaySpeed(SoundHandle handle, double to, Duration time) { + return wasmFadeRelativePlaySpeed(handle.id, to, time.toDouble()); + } + + @override + int schedulePause(SoundHandle handle, Duration duration) { + return wasmSchedulePause(handle.id, duration.toDouble()); + } + + @override + int scheduleStop(SoundHandle handle, Duration duration) { + return wasmScheduleStop(handle.id, duration.toDouble()); + } + + @override + int oscillateVolume( + SoundHandle handle, + double from, + double to, + Duration time, + ) { + return wasmOscillateVolume(handle.id, from, to, time.toDouble()); + } + + @override + int oscillatePan(SoundHandle handle, double from, double to, Duration time) { + return wasmOscillatePan(handle.id, from, to, time.toDouble()); + } + + @override + int oscillateRelativePlaySpeed( + SoundHandle handle, + double from, + double to, + Duration time, + ) { + return wasmOscillateRelativePlaySpeed(handle.id, from, to, time.toDouble()); + } + + @override + int oscillateGlobalVolume(double from, double to, Duration time) { + return wasmOscillateGlobalVolume(from, to, time.toDouble()); + } + + // /////////////////////////////////////// + // Filters + // /////////////////////////////////////// + + @override + ({PlayerErrors error, int index}) isFilterActive(FilterType filterType) { + // ignore: omit_local_variable_types + final idPtr = wasmMalloc(4); // 4 bytes for an int + final e = wasmIsFilterActive(filterType.index, idPtr); + final index = wasmGetI32Value(idPtr, 'i32'); + final ret = (error: PlayerErrors.values[e], index: index); + wasmFree(idPtr); + return ret; + } + + @override + ({PlayerErrors error, List names}) getFilterParamNames( + FilterType filterType, + ) { + final paramsCountPtr = wasmMalloc(4); // 4 bytes for an int + final namesPtr = wasmMalloc(30 * 20); // list of 30 String with 20 chars + final e = + wasmGetFilterParamNames(filterType.index, paramsCountPtr, namesPtr); + + final pNames = []; + var offsetPtr = 0; + final paramsCount = wasmGetI32Value(paramsCountPtr, '*'); + for (var i = 0; i < paramsCount; i++) { + final namePtr = wasmGetI32Value(namesPtr + offsetPtr, 'i32'); + final name = wasmUtf8ToString(namePtr); + offsetPtr += name.length; + + pNames.add(name); + } + + final ret = (error: PlayerErrors.values[e], names: pNames); + wasmFree(namesPtr); + wasmFree(paramsCountPtr); + return ret; + } + + @override + PlayerErrors addGlobalFilter(FilterType filterType) { + final e = wasmAddGlobalFilter(filterType.index); + return PlayerErrors.values[e]; + } + + @override + int removeGlobalFilter(FilterType filterType) { + return wasmRemoveGlobalFilter(filterType.index); + } + + @override + int setFilterParams(FilterType filterType, int attributeId, double value) { + return wasmSetFxParams(filterType.index, attributeId, value); + } + + @override + double getFilterParams(FilterType filterType, int attributeId) { + return wasmGetFxParams(filterType.index, attributeId); + } + + // ////////////////////////////////////// + // 3D audio methods + // ////////////////////////////////////// + + @override + ({PlayerErrors error, SoundHandle newHandle}) play3d( + SoundHash soundHash, + double posX, + double posY, + double posZ, { + double velX = 0, + double velY = 0, + double velZ = 0, + double volume = 1, + bool paused = false, + bool looping = false, + Duration loopingStartAt = Duration.zero, + }) { + final handlePtr = wasmMalloc(4); // 4 bytes for an int + final result = wasmPlay3d( + soundHash.hash, + posX, + posY, + posZ, + velX, + velY, + velZ, + volume, + paused ? 1 : 0, + looping ? 1 : 0, + loopingStartAt.toDouble(), + handlePtr, + ); + + /// "*" means unsigned int 32 + final newHandle = wasmGetI32Value(handlePtr, '*'); + final ret = + (error: PlayerErrors.values[result], newHandle: SoundHandle(newHandle)); + wasmFree(handlePtr); + + return ret; + } + + @override + void set3dSoundSpeed(double speed) { + return wasmSet3dSoundSpeed(speed); + } + + @override + double get3dSoundSpeed() { + return wasmGet3dSoundSpeed(); + } + + @override + void set3dListenerParameters( + double posX, + double posY, + double posZ, + double atX, + double atY, + double atZ, + double upX, + double upY, + double upZ, + double velocityX, + double velocityY, + double velocityZ, + ) { + return wasmSet3dListenerParameters( + posX, + posY, + posZ, + atX, + atY, + atZ, + upX, + upY, + upZ, + velocityX, + velocityY, + velocityZ, + ); + } + + @override + void set3dListenerPosition(double posX, double posY, double posZ) { + return wasmSet3dListenerPosition(posX, posY, posZ); + } + + @override + void set3dListenerAt(double atX, double atY, double atZ) { + return wasmSet3dListenerAt(atX, atY, atZ); + } + + @override + void set3dListenerUp(double upX, double upY, double upZ) { + return wasmSet3dListenerUp(upX, upY, upZ); + } + + @override + void set3dListenerVelocity( + double velocityX, + double velocityY, + double velocityZ, + ) { + return wasmSet3dListenerVelocity(velocityX, velocityY, velocityZ); + } + + @override + void set3dSourceParameters( + SoundHandle handle, + double posX, + double posY, + double posZ, + double velocityX, + double velocityY, + double velocityZ, + ) { + return wasmSet3dSourceParameters( + handle.id, + posX, + posY, + posZ, + velocityX, + velocityY, + velocityZ, + ); + } + + @override + void set3dSourcePosition( + SoundHandle handle, + double posX, + double posY, + double posZ, + ) { + return wasmSet3dSourcePosition(handle.id, posX, posY, posZ); + } + + @override + void set3dSourceVelocity( + SoundHandle handle, + double velocityX, + double velocityY, + double velocityZ, + ) { + return wasmSet3dSourceVelocity(handle.id, velocityX, velocityY, velocityZ); + } + + @override + void set3dSourceMinMaxDistance( + SoundHandle handle, + double minDistance, + double maxDistance, + ) { + return wasmSet3dSourceMinMaxDistance(handle.id, minDistance, maxDistance); + } + + @override + void set3dSourceAttenuation( + SoundHandle handle, + int attenuationModel, + double attenuationRolloffFactor, + ) { + return wasmSet3dSourceAttenuation( + handle.id, + attenuationModel, + attenuationRolloffFactor, + ); + } + + @override + void set3dSourceDopplerFactor(SoundHandle handle, double dopplerFactor) { + return wasmSet3dSourceDopplerFactor(handle.id, dopplerFactor); + } +} diff --git a/lib/src/bindings/js_extension.dart b/lib/src/bindings/js_extension.dart new file mode 100644 index 0000000..0306049 --- /dev/null +++ b/lib/src/bindings/js_extension.dart @@ -0,0 +1,438 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:js_interop'; +import 'package:web/web.dart' as web; + +@JS('Module._malloc') +external int wasmMalloc(int bytesCount); + +@JS('Module._free') +external void wasmFree(int ptrAddress); + +@JS('Module.getValue') +external int wasmGetI32Value(int ptrAddress, String type); + +@JS('Module.getValue') +external double wasmGetF32Value(int ptrAddress, String type); + +@JS('Module.UTF8ToString') +external String wasmUtf8ToString(int ptrAddress); + +@JS('Module.setValue') +external void wasmSetValue(int ptrAddress, int value, String type); + +@JS('Module.cwrap') +external JSFunction wasmCwrap( + JSString fName, + JSString returnType, + JSArray argTypes, +); + +@JS('Module.ccall') +external JSFunction wasmCccall( + JSString fName, + JSString returnType, + JSArray argTypes, + JSArray args, +); + +@JS('Module._createWorkerInWasm') +external void wasmCreateWorkerInWasm(); + +@JS('Module._sendToWorker') +external void wasmSendToWorker(int message, int value); + +@JS('Module.wasmWorker') +external web.Worker wasmWorker; + +@JS('Module._initEngine') +external int wasmInitEngine(); + +@JS('Module._dispose') +external void wasmDeinit(); + +@JS('Module._isInited') +external int wasmIsInited(); + +@JS('Module._loadFile') +external int wasmLoadFile( + int completeFileNamePtr, + int loadIntoMem, + int hashPtr, +); + +@JS('Module._loadMem') +external int wasmLoadMem( + int uniqueNamePtr, + int memPtr, + int length, + int loadIntoMem, + int hashPtr, +); + +@JS('Module._loadWaveform') +external int wasmLoadWaveform( + int waveform, + // ignore: avoid_positional_boolean_parameters + bool superWave, + double scale, + double detune, + int hashPtr, +); + +@JS('Module._setWaveformScale') +external void wasmSetWaveformScale(int soundHash, double newScale); + +@JS('Module._setWaveformDetune') +external void wasmSetWaveformDetune(int soundHash, double newDetune); + +@JS('Module._setWaveformFreq') +external void wasmSetWaveformFreq(int soundHash, double newFreq); + +@JS('Module._setSuperWave') +external void wasmSetSuperWave(int soundHash, int superwave); + +@JS('Module._setWaveform') +external void wasmSetWaveform(int soundHash, int newWaveform); + +@JS('Module._speechText') +external int wasmSpeechText(int textToSpeechPtr, int handlePtr); + +@JS('Module._pauseSwitch') +external void wasmPauseSwitch(int handle); + +@JS('Module._setPause') +external void wasmSetPause(int handle, int pause); + +@JS('Module._getPause') +external int wasmGetPause(int handle); + +@JS('Module._setRelativePlaySpeed') +external void wasmSetRelativePlaySpeed(int handle, double speed); + +@JS('Module._getRelativePlaySpeed') +external double wasmGetRelativePlaySpeed(int handle); + +@JS('Module._play') +external int wasmPlay( + int soundHash, + double volume, + double pan, + // ignore: avoid_positional_boolean_parameters + bool paused, + bool looping, + double loopingStartAt, + int handlePtr, +); + +@JS('Module._stop') +external void wasmStop(int handle); + +@JS('Module._disposeSound') +external void wasmDisposeSound(int soundHash); + +@JS('Module._disposeAllSound') +external void wasmDisposeAllSound(); + +@JS('Module._getLooping') +external int wasmGetLooping(int handle); + +@JS('Module._setLooping') +external void wasmSetLooping(int handle, int enable); + +@JS('Module._getLoopPoint') +external double wasmGetLoopPoint(int handle); + +@JS('Module._setLoopPoint') +external void wasmSetLoopPoint(int handle, double time); + +@JS('Module._setVisualizationEnabled') +external void wasmSetVisualizationEnabled(int enabled); + +@JS('Module._getVisualizationEnabled') +external int wasmGetVisualizationEnabled(); + +@JS('Module._getWave') +external void wasmGetWave(int samplesPtr); + +@JS('Module._getFft') +external void wasmGetFft(int samplesPtr); + +@JS('Module._setFftSmoothing') +external void wasmSetFftSmoothing(double smooth); + +@JS('Module._getCaptureFft') +external void wasmGetCaptureFft(int samplesPtr); + +@JS('Module._getCaptureWave') +external void wasmGetCaptureWave(int samplesPtr); + +@JS('Module._getAudioTexture') +external void wasmGetAudioTexture(int samplesPtr); + +@JS('Module._getAudioTexture2D') +external int wasmGetAudioTexture2D(int samplesPtr); + +@JS('Module._getTextureValue') +external double wasmGetTextureValue(int row, int column); + +@JS('Module._getCaptureTexture') +external void wasmGetCaptureAudioTexture(int samplesPtr); + +@JS('Module._getCaptureTextureValue') +external double wasmGetCaptureTextureValue(int row, int column); + +@JS('Module._getCaptureAudioTexture2D') +external int wasmGetCaptureAudioTexture2D(int samplesPtr); + +@JS('Module._setCaptureFftSmoothing') +external int wasmSetCaptureFftSmoothing(double smooth); + +@JS('Module._getLength') +external double wasmGetLength(int soundHash); + +@JS('Module._seek') +external int wasmSeek(int handle, double time); + +@JS('Module._getPosition') +external double wasmGetPosition(int handle); + +@JS('Module._getGlobalVolume') +external double wasmGetGlobalVolume(); + +@JS('Module._setGlobalVolume') +external int wasmSetGlobalVolume(double volume); + +@JS('Module._getVolume') +external double wasmGetVolume(int handle); + +@JS('Module._setVolume') +external int wasmSetVolume(int handle, double volume); + +@JS('Module._getPan') +external double wasmGetPan(int handle); + +@JS('Module._setPan') +external void wasmSetPan(int handle, double pan); + +@JS('Module._setPanAbsolute') +external void wasmSetPanAbsolute(int handle, double panLeft, double panRight); + +@JS('Module._getIsValidVoiceHandle') +external int wasmGetIsValidVoiceHandle(int handle); + +@JS('Module._getActiveVoiceCount') +external int wasmGetActiveVoiceCount(); + +@JS('Module._countAudioSource') +external int wasmCountAudioSource(int soundHash); + +@JS('Module._getVoiceCount') +external int wasmGetVoiceCount(); + +@JS('Module._getProtectVoice') +external int wasmGetProtectVoice(int handle); + +@JS('Module._setProtectVoice') +external void wasmSetProtectVoice(int handle, int protect); + +@JS('Module._getMaxActiveVoiceCount') +external int wasmGetMaxActiveVoiceCount(); + +@JS('Module._setMaxActiveVoiceCount') +external void wasmSetMaxActiveVoiceCount(int maxVoiceCount); + +@JS('Module._fadeGlobalVolume') +external int wasmFadeGlobalVolume(double to, double duration); + +@JS('Module._fadeVolume') +external int wasmFadeVolume(int handle, double to, double duration); + +@JS('Module._fadePan') +external int wasmFadePan(int handle, double to, double duration); + +@JS('Module._fadeRelativePlaySpeed') +external int wasmFadeRelativePlaySpeed(int handle, double to, double duration); + +@JS('Module._schedulePause') +external int wasmSchedulePause(int handle, double duration); + +@JS('Module._scheduleStop') +external int wasmScheduleStop(int handle, double duration); + +@JS('Module._oscillateVolume') +external int wasmOscillateVolume( + int handle, + double from, + double to, + double time, +); + +@JS('Module._oscillatePan') +external int wasmOscillatePan(int handle, double from, double to, double time); + +@JS('Module._oscillateRelativePlaySpeed') +external int wasmOscillateRelativePlaySpeed( + int handle, + double from, + double to, + double time, +); + +@JS('Module._oscillateGlobalVolume') +external int wasmOscillateGlobalVolume(double from, double to, double time); + +@JS('Module._isFilterActive') +external int wasmIsFilterActive(int filterType, int idPtr); + +@JS('Module._getFilterParamNames') +external int wasmGetFilterParamNames( + int filterType, + int paramsCountPtr, + int namesPtr, +); + +@JS('Module._addGlobalFilter') +external int wasmAddGlobalFilter(int filterType); + +@JS('Module._removeGlobalFilter') +external int wasmRemoveGlobalFilter(int filterType); + +@JS('Module._setFxParams') +external int wasmSetFxParams(int filterType, int attributeId, double value); + +@JS('Module._getFxParams') +external double wasmGetFxParams(int filterType, int attributeId); + +@JS('Module._play3d') +external int wasmPlay3d( + int soundHash, + double posX, + double posY, + double posZ, + double velX, + double velY, + double velZ, + double volume, + int paused, + int looping, + double loopingStartAt, + int handlePtr, +); + +@JS('Module._set3dSoundSpeed') +external void wasmSet3dSoundSpeed(double speed); + +@JS('Module._get3dSoundSpeed') +external double wasmGet3dSoundSpeed(); + +@JS('Module._set3dListenerParameters') +external void wasmSet3dListenerParameters( + double posX, + double posY, + double posZ, + double atX, + double atY, + double atZ, + double upX, + double upY, + double upZ, + double velocityX, + double velocityY, + double velocityZ, +); + +@JS('Module._set3dListenerPosition') +external void wasmSet3dListenerPosition(double posX, double posY, double posZ); + +@JS('Module._set3dListenerAt') +external void wasmSet3dListenerAt(double atX, double atY, double atZ); + +@JS('Module._set3dListenerUp') +external void wasmSet3dListenerUp(double upX, double upY, double upZ); + +@JS('Module._set3dListenerVelocity') +external void wasmSet3dListenerVelocity( + double velocityX, + double velocityY, + double velocityZ, +); + +@JS('Module._set3dSourceParameters') +external void wasmSet3dSourceParameters( + int handle, + double posX, + double posY, + double posZ, + double velocityX, + double velocityY, + double velocityZ, +); + +@JS('Module._set3dSourcePosition') +external void wasmSet3dSourcePosition( + int handle, + double posX, + double posY, + double posZ, +); + +@JS('Module._set3dSourceVelocity') +external void wasmSet3dSourceVelocity( + int handle, + double velocityX, + double velocityY, + double velocityZ, +); + +@JS('Module._set3dSourceMinMaxDistance') +external void wasmSet3dSourceMinMaxDistance( + int handle, + double minDistance, + double maxDistance, +); + +@JS('Module._set3dSourceAttenuation') +external void wasmSet3dSourceAttenuation( + int handle, + int attenuationModel, + double attenuationRolloffFactor, +); + +@JS('Module._set3dSourceDopplerFactor') +external void wasmSet3dSourceDopplerFactor(int handle, double dopplerFactor); + +// /////////////////////////// +// Capture +// /////////////////////////// +@JS('Module._listCaptureDevices') +external void wasmListCaptureDevices( + int namesPtr, + int isDefaultPtr, + int nDevicePtr, +); + +@JS('Module._freeListCaptureDevices') +external void wasmFreeListCaptureDevices( + int namesPtr, + int isDefaultPtr, + int nDevice, +); + +@JS('Module._initCapture') +external int wasmInitCapture(int deviceID); + +@JS('Module._disposeCapture') +external void wasmDisposeCapture(); + +@JS('Module._isCaptureInited') +external int wasmIsCaptureInited(); + +@JS('Module._isCaptureStarted') +external int wasmIsCaptureStarted(); + +@JS('Module._startCapture') +external int wasmStartCapture(); + +@JS('Module._stopCapture') +external int wasmStopCapture(); diff --git a/lib/src/bindings/soloud_controller.dart b/lib/src/bindings/soloud_controller.dart new file mode 100644 index 0000000..94d1eea --- /dev/null +++ b/lib/src/bindings/soloud_controller.dart @@ -0,0 +1,2 @@ +export 'package:flutter_soloud/src/bindings/soloud_controller_ffi.dart' + if (dart.library.js_interop) 'package:flutter_soloud/src/bindings/soloud_controller_web.dart'; diff --git a/lib/src/soloud_controller.dart b/lib/src/bindings/soloud_controller_ffi.dart similarity index 80% rename from lib/src/soloud_controller.dart rename to lib/src/bindings/soloud_controller_ffi.dart index aaf3edf..3be2aaa 100644 --- a/lib/src/soloud_controller.dart +++ b/lib/src/bindings/soloud_controller_ffi.dart @@ -1,31 +1,17 @@ +// ignore_for_file: public_member_api_docs + import 'dart:ffi' as ffi; import 'dart:io'; -import 'package:flutter_soloud/src/bindings_capture_ffi.dart'; -import 'package:flutter_soloud/src/bindings_player_ffi.dart'; +import 'package:flutter_soloud/src/bindings/bindings_capture_ffi.dart'; +import 'package:flutter_soloud/src/bindings/bindings_player_ffi.dart'; /// Controller that expose method channel and FFI class SoLoudController { - /// factory SoLoudController() => _instance ??= SoLoudController._(); SoLoudController._() { - initialize(); - } - - static SoLoudController? _instance; - - /// - late ffi.DynamicLibrary nativeLib; - - /// - late final FlutterSoLoudFfi soLoudFFI; - - /// - late final FlutterCaptureFfi captureFFI; - - /// - void initialize() { + /// Initialize lib nativeLib = Platform.isLinux ? ffi.DynamicLibrary.open('libflutter_soloud_plugin.so') : (Platform.isAndroid @@ -36,4 +22,12 @@ class SoLoudController { soLoudFFI = FlutterSoLoudFfi.fromLookup(nativeLib.lookup); captureFFI = FlutterCaptureFfi.fromLookup(nativeLib.lookup); } + + static SoLoudController? _instance; + + late ffi.DynamicLibrary nativeLib; + + late final FlutterSoLoudFfi soLoudFFI; + + late final FlutterCaptureFfi captureFFI; } diff --git a/lib/src/bindings/soloud_controller_web.dart b/lib/src/bindings/soloud_controller_web.dart new file mode 100644 index 0000000..65a787a --- /dev/null +++ b/lib/src/bindings/soloud_controller_web.dart @@ -0,0 +1,22 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter_soloud/src/bindings/bindings_capture_web.dart'; + +import 'package:flutter_soloud/src/bindings/bindings_player_web.dart'; + +/// Controller that expose method channel and FFI +class SoLoudController { + factory SoLoudController() => _instance ??= SoLoudController._(); + + SoLoudController._(); + + static SoLoudController? _instance; + + final FlutterSoLoudWeb _soLoudFFI = FlutterSoLoudWeb(); + + FlutterSoLoudWeb get soLoudFFI => _soLoudFFI; + + final FlutterCaptureWeb _captureFFI = FlutterCaptureWeb(); + + FlutterCaptureWeb get captureFFI => _captureFFI; +} diff --git a/lib/src/bindings_capture_ffi.dart b/lib/src/bindings_capture_ffi.dart deleted file mode 100644 index 7ce327b..0000000 --- a/lib/src/bindings_capture_ffi.dart +++ /dev/null @@ -1,162 +0,0 @@ -// ignore_for_file: always_specify_types -// ignore_for_file: camel_case_types -// ignore_for_file: non_constant_identifier_names - -// Generated by `package:ffigen`. -// ignore_for_file: type=lint -import 'dart:ffi' as ffi; - -import 'package:ffi/ffi.dart'; -import 'package:flutter_soloud/src/enums.dart'; - -/// CaptureDevice struct exposed in C -final class _CaptureDevice extends ffi.Struct { - external ffi.Pointer name; - - @ffi.UnsignedInt() - external int isDefault; -} - -/// FFI bindings to capture with miniaudio -class FlutterCaptureFfi { - /// Holds the symbol lookup function. - final ffi.Pointer Function(String symbolName) - _lookup; - - /// The symbols are looked up in [dynamicLibrary]. - FlutterCaptureFfi(ffi.DynamicLibrary dynamicLibrary) - : _lookup = dynamicLibrary.lookup; - - /// The symbols are looked up with [lookup]. - FlutterCaptureFfi.fromLookup( - ffi.Pointer Function(String symbolName) - lookup) - : _lookup = lookup; - - /// --------------------- copy here the new functions to generate - List listCaptureDevices() { - List ret = []; - ffi.Pointer> devices = - calloc(ffi.sizeOf<_CaptureDevice>()); - ffi.Pointer n_devices = calloc(); - - _listCaptureDevices( - devices, - n_devices, - ); - - int ndev = n_devices.value; - for (int i = 0; i < ndev; i++) { - var s = (devices + i).value.ref.name.cast().toDartString(); - var n = (devices + i).value.ref.isDefault; - ret.add(CaptureDevice(s, n == 1 ? true : false)); - } - - /// free allocated memory done in C - /// this work on linux and android, not on win - // for (int i = 0; i < ndev; i++) { - // calloc.free(devices.elementAt(i).value.ref.name); - // calloc.free(devices.elementAt(i).value); - // } - _freeListCaptureDevices( - devices, - ndev, - ); - - calloc.free(devices); - calloc.free(n_devices); - return ret; - } - - late final _listCaptureDevicesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer>, - ffi.Pointer)>>('listCaptureDevices'); - late final _listCaptureDevices = _listCaptureDevicesPtr.asFunction< - void Function( - ffi.Pointer>, ffi.Pointer)>(); - - late final _freeListCaptureDevicesPtr = _lookup< - ffi.NativeFunction< - ffi.Void Function(ffi.Pointer>, - ffi.Int)>>('freeListCaptureDevices'); - late final _freeListCaptureDevices = _freeListCaptureDevicesPtr.asFunction< - void Function(ffi.Pointer>, int)>(); - - /// - CaptureErrors initCapture(int deviceID) { - final e = _initCapture(deviceID); - return CaptureErrors.values[e]; - } - - late final _initCapturePtr = - _lookup>('initCapture'); - late final _initCapture = _initCapturePtr.asFunction(); - - void disposeCapture() { - return _disposeCapture(); - } - - late final _disposeCapturePtr = - _lookup>('disposeCapture'); - late final _disposeCapture = _disposeCapturePtr.asFunction(); - - bool isCaptureInited() { - return _isCaptureInited() == 1 ? true : false; - } - - late final _isCaptureInitedPtr = - _lookup>('isCaptureInited'); - late final _isCaptureInited = - _isCaptureInitedPtr.asFunction(); - - bool isCaptureStarted() { - return _isCaptureStarted() == 1 ? true : false; - } - - late final _isCaptureStartedPtr = - _lookup>('isCaptureStarted'); - late final _isCaptureStarted = - _isCaptureStartedPtr.asFunction(); - - CaptureErrors startCapture() { - return CaptureErrors.values[_startCapture()]; - } - - late final _startCapturePtr = - _lookup>('startCapture'); - late final _startCapture = _startCapturePtr.asFunction(); - - CaptureErrors stopCapture() { - return CaptureErrors.values[_stopCapture()]; - } - - late final _stopCapturePtr = - _lookup>('stopCapture'); - late final _stopCapture = _stopCapturePtr.asFunction(); - - CaptureErrors getCaptureAudioTexture2D( - ffi.Pointer> samples, - ) { - int ret = _getCaptureAudioTexture2D(samples); - return CaptureErrors.values[ret]; - } - - late final _getCaptureAudioTexture2DPtr = _lookup< - ffi.NativeFunction< - ffi.Int32 Function(ffi.Pointer>)>>( - 'getCaptureAudioTexture2D'); - late final _getCaptureAudioTexture2D = _getCaptureAudioTexture2DPtr - .asFunction>)>(); - - CaptureErrors setCaptureFftSmoothing(double smooth) { - int ret = _setCaptureFftSmoothing(smooth); - return CaptureErrors.values[ret]; - } - - late final _setCaptureFftSmoothingPtr = - _lookup>( - 'setCaptureFftSmoothing'); - late final _setCaptureFftSmoothing = - _setCaptureFftSmoothingPtr.asFunction(); -} diff --git a/lib/src/enums.dart b/lib/src/enums.dart index 884ae39..e433c9a 100644 --- a/lib/src/enums.dart +++ b/lib/src/enums.dart @@ -24,6 +24,10 @@ enum CaptureErrors { /// Capture not yet initialized captureNotInited, + /// null pointer. Could happens when passing a non initialized + /// pointer (with calloc()) to retrieve FFT or wave data + failedToStartDevice, + /// null pointer. Could happens when passing a non initialized /// pointer (with calloc()) to retrieve FFT or wave data nullPointer; @@ -37,10 +41,11 @@ enum CaptureErrors { return 'Capture failed to initialize'; case CaptureErrors.captureNotInited: return 'Capture not yet initialized'; + case CaptureErrors.failedToStartDevice: + return 'Failed to start capture device.'; case CaptureErrors.nullPointer: return 'Capture null pointer error. Could happens when passing a non ' - 'initialized pointer (with calloc()) to retrieve FFT or wave data. ' - 'Or, setVisualization has not been enabled.'; + 'initialized pointer (with calloc()) to retrieve FFT or wave data.'; } } diff --git a/lib/src/exceptions/exceptions.dart b/lib/src/exceptions/exceptions.dart index e4e1afc..33767c6 100644 --- a/lib/src/exceptions/exceptions.dart +++ b/lib/src/exceptions/exceptions.dart @@ -103,4 +103,28 @@ abstract class SoLoudCppException extends SoLoudException { return const SoLoudSoundHandleNotFoundCppException(); } } + + /// Takes a [CaptureErrors] enum value and returns a corresponding exception. + /// This is useful when we need to convert a C++ error to a Dart exception. + /// + /// If [error] is "CaptureErrors.noError", this constructor throws + /// an [ArgumentError]. + factory SoLoudCppException.fromCaptureError(CaptureErrors error) { + switch (error) { + case CaptureErrors.captureNoError: + throw ArgumentError( + 'Trying to create an exception from CaptureErrors.noError. ' + 'This is a bug in the library. Please report it.', + 'error', + ); + case CaptureErrors.captureInitFailed: + return const SoLoudCaptureInitFailedException(); + case CaptureErrors.captureNotInited: + return const SoLoudCaptureNotYetInitializededException(); + case CaptureErrors.failedToStartDevice: + return const SoLoudCaptureFailedToStartException(); + case CaptureErrors.nullPointer: + return const SoLoudCaptureNullPointerException(); + } + } } diff --git a/lib/src/exceptions/exceptions_from_cpp.dart b/lib/src/exceptions/exceptions_from_cpp.dart index e3afed8..f9274c4 100644 --- a/lib/src/exceptions/exceptions_from_cpp.dart +++ b/lib/src/exceptions/exceptions_from_cpp.dart @@ -1,8 +1,8 @@ part of 'exceptions.dart'; -// /////////////////////// -// / C++-side exceptions / -// /////////////////////// +// ////////////////////////////////////// +// / C++-side exceptions for the player / +// ////////////////////////////////////// /// An exception that is thrown when an invalid parameter was passed /// to SoLoud (C++). @@ -175,3 +175,51 @@ class SoLoudSoundHandleNotFoundCppException extends SoLoudCppException { String get description => 'The sound handle is not found ' '(on the C++ side).'; } + +// /////////////////////////////// +// / C++-side exceptions capture / +// /////////////////////////////// + +/// An exception that is thrown when SoLoud (C++) cannot initialize +/// a capture device. +class SoLoudCaptureInitFailedException extends SoLoudCppException { + /// Creates a new [SoLoudCaptureInitFailedException]. + const SoLoudCaptureInitFailedException([super.message]); + + @override + String get description => 'The capture device has failed to initialize ' + '(on the C++ side).'; +} + +/// An exception that is thrown when SoLoud (C++) has not yet been initialized +/// and the operation asked to perform cannot be performed. +class SoLoudCaptureNotYetInitializededException extends SoLoudCppException { + /// Creates a new [SoLoudCaptureNotYetInitializededException]. + const SoLoudCaptureNotYetInitializededException([super.message]); + + @override + String get description => 'The capture device has not yet been initialized ' + '(on the C++ side).'; +} + +/// An exception that is thrown when SoLoud (C++) when the start device +/// task gives problems. +class SoLoudCaptureFailedToStartException extends SoLoudCppException { + /// Creates a new [SoLoudCaptureFailedToStartException]. + const SoLoudCaptureFailedToStartException([super.message]); + + @override + String get description => 'The capture device failed to start ' + '(on the C++ side).'; +} + +/// Capture null pointer error. Could happens when passing a non initialized +/// pointer (with calloc()) to retrieve FFT or wave data. +class SoLoudCaptureNullPointerException extends SoLoudCppException { + /// Creates a new [SoLoudCaptureNullPointerException]. + const SoLoudCaptureNullPointerException([super.message]); + @override + String get description => 'Capture null pointer error. Could happens ' + 'when passing a non initialized pointer (with calloc()) to retrieve ' + 'FFT or wave data (on the C++ side).'; +} diff --git a/lib/src/soloud.dart b/lib/src/soloud.dart index 4a52849..50ccdf5 100644 --- a/lib/src/soloud.dart +++ b/lib/src/soloud.dart @@ -1,16 +1,15 @@ // ignore_for_file: require_trailing_commas, avoid_positional_boolean_parameters import 'dart:async'; -import 'dart:ffi' as ffi; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_soloud/src/audio_source.dart'; +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.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'; @@ -23,7 +22,7 @@ import 'package:meta/meta.dart'; /// This class has a singleton [instance] which represents the (also singleton) /// instance of the SoLoud (C++) engine. /// -/// For methods that _capture_ sounds, use [SoLoudCapture]. +/// For methods that _capture_ sounds, use "SoLoudCapture". interface class SoLoud { /// The private constructor of [SoLoud]. This prevents developers from /// instantiating new instances. @@ -31,6 +30,8 @@ interface class SoLoud { static final Logger _log = Logger('flutter_soloud.SoLoud'); + final _controller = SoLoudController(); + /// 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. @@ -101,10 +102,10 @@ interface class SoLoud { /// 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. // TODO(filip): related to `get initialized`. This line below is the old one. - // bool get isInitialized => _isInitialized; + bool get isInitialized => _isInitialized; // TODO(filip): this line below is the new one I leaved to let the /// plugin to work. - bool get isInitialized => SoLoudController().soLoudFFI.isInited(); + // bool get isInitialized => _controller.soLoudFFI.isInited(); /// The completer for an initialization in progress. /// @@ -202,6 +203,7 @@ interface class SoLoud { /// unnecessary, as the amount of data will be finite. /// The default is `false`. Future init({ + // TODO(filip): remove deprecation? @Deprecated('timeout is not used anymore.') Duration timeout = const Duration(seconds: 10), bool automaticCleanup = false, @@ -231,9 +233,11 @@ interface class SoLoud { // Initialize native callbacks _initializeNativeCallbacks(); - final error = SoLoudController().soLoudFFI.initEngine(); + final error = _controller.soLoudFFI.initEngine(); _logPlayerError(error, from: 'initialize() result'); 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` @@ -242,7 +246,6 @@ interface class SoLoud { // Initialize [SoLoudLoader] _loader.automaticCleanup = automaticCleanup; - // TODO(filip): The Loader is not compatible with web! await _loader.initialize(); } else { _log.severe('initialize() failed with error: $error'); @@ -257,8 +260,9 @@ interface class SoLoud { void deinit() { _log.finest('deinit() called'); - SoLoudController().soLoudFFI.disposeAllSound(); - SoLoudController().soLoudFFI.deinit(); + _isInitialized = false; + _controller.soLoudFFI.disposeAllSound(); + _controller.soLoudFFI.deinit(); _activeSounds.clear(); } @@ -283,14 +287,14 @@ interface class SoLoud { /// From within these callbacks a new stream event is added and listened here. // TODO(filip): 'setDartEventCallbacks()' can be called more then once, // please take a look at the listeners if you find a better way - // to manage then only once. + // to manage them only once. void _initializeNativeCallbacks() { // Initialize callbacks. - SoLoudController().soLoudFFI.setDartEventCallbacks(); + _controller.soLoudFFI.setDartEventCallbacks(); // Listen when a handle becomes invalid becaus has been stopped/ended - if (!SoLoudController().soLoudFFI.voiceEndedEventController.hasListener) { - SoLoudController().soLoudFFI.voiceEndedEvents.listen((handle) { + if (!_controller.soLoudFFI.voiceEndedEventController.hasListener) { + _controller.soLoudFFI.voiceEndedEvents.listen((handle) { // Remove this UNIQUE [handle] from the `AudioSource` that own it. final soundHandleFound = _isHandlePresent(SoundHandle(handle)); @@ -318,8 +322,8 @@ interface class SoLoud { } // Listen when a file has been loaded - if (!SoLoudController().soLoudFFI.fileLoadedEventsController.hasListener) { - SoLoudController().soLoudFFI.fileLoadedEvents.listen((result) { + if (!_controller.soLoudFFI.fileLoadedEventsController.hasListener) { + _controller.soLoudFFI.fileLoadedEvents.listen((result) { final exists = loadedFileCompleters.containsKey(result['completeFileName']); if (exists) { @@ -363,8 +367,8 @@ interface class SoLoud { // This doesn't work on Android. See "ma_device_notification_proc" // in miniaudio.h. Only `started` and `stopped` are working. // Leaving this commented out for futher investigation. - // if (!SoLoudController().soLoudFFI.stateChangedController.hasListener) { - // SoLoudController().soLoudFFI.stateChangedEvents.listen((newState) { + // if (!_controller.soLoudFFI.stateChangedController.hasListener) { + // _controller.soLoudFFI.stateChangedEvents.listen((newState) { // _log.fine(() => 'Audio engine state changed: $newState'); // }); // } @@ -401,7 +405,7 @@ interface class SoLoud { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.loadFile(path, mode); + _controller.soLoudFFI.loadFile(path, mode); final completer = Completer(); loadedFileCompleters.addAll({ @@ -419,15 +423,29 @@ interface class SoLoud { /// Provide a [path] of the file to be used as a reference to distinguis /// this [buffer]. /// - /// The [buffer] represents the bytes of an audio file. It could be also a - /// simple WAV format sequence of manually generated bytes. + /// The [buffer] represents the bytes of a supported audio file (not + /// RAW data). + /// It could be also a simple WAV format sequence of manually generated bytes. + /// + /// 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]. + /// IMPORTANT: [LoadMode.memory] used the on web platform could cause UI + /// freeze problems. + /// + /// This is the only choice to load a file when using this plugin on the web + /// because browsers cannot read directly files from the loal storage. /// - /// It is useful when using this plugin on the web browsers because - /// they cannot read directly files in the loal storage. + /// Throws [SoLoudNotInitializedException] if the engine is not initialized. Future loadMem( String path, - Uint8List buffer, - ) async { + Uint8List buffer, { + LoadMode mode = LoadMode.memory, + }) async { if (!isInitialized) { throw const SoLoudNotInitializedException(); } @@ -437,12 +455,12 @@ interface class SoLoud { path: completer, }); - final ret = SoLoudController().soLoudFFI.loadMem(path, buffer); + final ret = _controller.soLoudFFI.loadMem(path, buffer, mode); /// There is not a callback in cpp that is supposed to add the /// "load file event". Manually send this event to have only one /// place to do this "loaded" job. - SoLoudController().soLoudFFI.fileLoadedEventsController.add({ + _controller.soLoudFFI.fileLoadedEventsController.add({ 'error': ret.error.index, 'completeFileName': path, 'hash': ret.soundHash.hash, @@ -482,9 +500,13 @@ interface class SoLoud { throw const SoLoudNotInitializedException(); } - final file = await _loader.loadAsset(key, assetBundle: assetBundle); + final newAudioSource = await _loader.loadAsset( + key, + mode, + assetBundle: assetBundle, + ); - return loadFile(file.absolute.path, mode: mode); + return newAudioSource; } /// Load a new sound to be played once or multiple times later, from @@ -520,9 +542,10 @@ interface class SoLoud { throw const SoLoudNotInitializedException(); } - final file = await _loader.loadUrl(url, httpClient: httpClient); + final newAudioSource = + await _loader.loadUrl(url, mode, httpClient: httpClient); - return loadFile(file.absolute.path, mode: mode); + return newAudioSource; } /// Load a new waveform to be played once or multiple times later. @@ -545,12 +568,12 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.loadWaveform( - waveform, - superWave, - scale, - detune, - ); + final ret = _controller.soLoudFFI.loadWaveform( + waveform, + superWave, + scale, + detune, + ); if (ret.error == PlayerErrors.noError) { final newSound = AudioSource(ret.soundHash); @@ -571,7 +594,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setWaveform(sound.soundHash, newWaveform); + _controller.soLoudFFI.setWaveform(sound.soundHash, newWaveform); } /// If this sound is a `superWave` you can change the scale at runtime. @@ -584,7 +607,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setWaveformScale(sound.soundHash, newScale); + _controller.soLoudFFI.setWaveformScale(sound.soundHash, newScale); } /// If this sound is a `superWave` you can change the detune at runtime. @@ -597,7 +620,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setWaveformDetune(sound.soundHash, newDetune); + _controller.soLoudFFI.setWaveformDetune(sound.soundHash, newDetune); } /// Set the frequency of the given waveform sound. @@ -610,7 +633,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setWaveformFreq(sound.soundHash, newFrequency); + _controller.soLoudFFI.setWaveformFreq(sound.soundHash, newFrequency); } /// Set the given waveform sound's super wave flag. @@ -623,10 +646,10 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setWaveformSuperWave( - sound.soundHash, - superwave ? 1 : 0, - ); + _controller.soLoudFFI.setWaveformSuperWave( + sound.soundHash, + superwave ? 1 : 0, + ); } /// Create a new audio source from the given [textToSpeech]. @@ -638,7 +661,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.speechText(textToSpeech); + final ret = _controller.soLoudFFI.speechText(textToSpeech); _logPlayerError(ret.error, from: 'speechText() result'); if (ret.error == PlayerErrors.noError) { @@ -684,14 +707,14 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.play( - sound.soundHash, - volume: volume, - pan: pan, - paused: paused, - looping: looping, - loopingStartAt: loopingStartAt, - ); + final ret = _controller.soLoudFFI.play( + sound.soundHash, + volume: volume, + pan: pan, + paused: paused, + looping: looping, + loopingStartAt: loopingStartAt, + ); _logPlayerError(ret.error, from: 'play()'); if (ret.error != PlayerErrors.noError) { throw SoLoudCppException.fromPlayerError(ret.error); @@ -719,7 +742,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.pauseSwitch(handle); + _controller.soLoudFFI.pauseSwitch(handle); } /// Pause or unpause a currently playing sound identified by [handle]. @@ -729,7 +752,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setPause(handle, pause ? 1 : 0); + _controller.soLoudFFI.setPause(handle, pause ? 1 : 0); } /// Gets the pause state of a currently playing sound identified by [handle]. @@ -739,7 +762,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getPause(handle); + return _controller.soLoudFFI.getPause(handle); } /// Set a sound's relative play speed. @@ -748,7 +771,7 @@ interface class SoLoud { /// and the new [speed]. /// /// Setting the speed value to `0` will cause undefined behavior, - /// likely a crash. The lower limit is clamped to be >=0.05 silently. + /// likely a crash. The lower limit is clamped to 0.05 silently. /// /// This changes the effective sample rate /// while leaving the base sample rate alone. @@ -762,7 +785,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setRelativePlaySpeed(handle, speed); + _controller.soLoudFFI.setRelativePlaySpeed(handle, speed); } /// Get a sound's relative play speed. Provide the sound instance via @@ -773,7 +796,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getRelativePlaySpeed(handle); + return _controller.soLoudFFI.getRelativePlaySpeed(handle); } /// Stop a currently playing sound identified by [handle] @@ -789,7 +812,7 @@ interface class SoLoud { final completer = Completer(); voiceEndedCompleters[handle] = completer; - SoLoudController().soLoudFFI.stop(handle); + _controller.soLoudFFI.stop(handle); return completer.future .timeout(const Duration(milliseconds: 300)) @@ -813,13 +836,15 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.disposeSound(source.soundHash); + _controller.soLoudFFI.disposeSound(source.soundHash); - source.soundEventsController.add(( - event: SoundEventType.soundDisposed, - sound: source, - handle: SoundHandle.error(), - )); + if (!source.soundEventsController.isClosed) { + source.soundEventsController.add(( + event: SoundEventType.soundDisposed, + sound: source, + handle: SoundHandle.error(), + )); + } await source.soundEventsController.close(); /// remove the sound with [soundHash] @@ -840,7 +865,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.disposeAllSound(); + _controller.soLoudFFI.disposeAllSound(); for (final sound in _activeSounds) { sound.soundEventsController.add(( @@ -865,7 +890,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getLooping(handle); + return _controller.soLoudFFI.getLooping(handle); } /// Set the looping flag of a currently playing sound, provided via @@ -876,7 +901,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setLooping(handle, enable); + _controller.soLoudFFI.setLooping(handle, enable); } /// Get the loop point value of a currently playing sound, provided via @@ -889,7 +914,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getLoopPoint(handle); + return _controller.soLoudFFI.getLoopPoint(handle); } /// Set the loop point of a currently playing sound, provided via @@ -902,7 +927,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setLoopPoint(handle, time); + _controller.soLoudFFI.setLoopPoint(handle, time); } /// Enable or disable visualization. @@ -916,7 +941,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setVisualizationEnabled(enabled); + _controller.soLoudFFI.setVisualizationEnabled(enabled); _isVisualizationEnabled = enabled; } @@ -929,7 +954,9 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getVisualizationEnabled(); + // ignore: join_return_with_assignment + _isVisualizationEnabled = _controller.soLoudFFI.getVisualizationEnabled(); + return _isVisualizationEnabled; } /// Get the length of a loaded audio [source]. @@ -941,7 +968,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getLength(source.soundHash); + return _controller.soLoudFFI.getLength(source.soundHash); } /// Seek a currently playing sound instance, provided via its [handle]. @@ -969,7 +996,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.seek(handle, time); + final ret = _controller.soLoudFFI.seek(handle, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'seek(): $error'); @@ -989,7 +1016,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getPosition(handle); + return _controller.soLoudFFI.getPosition(handle); } /// Gets the current global volume. @@ -1008,7 +1035,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getGlobalVolume(); + return _controller.soLoudFFI.getGlobalVolume(); } /// Sets the global volume which affects all sounds. @@ -1027,7 +1054,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.setGlobalVolume(volume); + final ret = _controller.soLoudFFI.setGlobalVolume(volume); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'setGlobalVolume(): $error'); @@ -1051,7 +1078,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getVolume(handle); + return _controller.soLoudFFI.getVolume(handle); } /// Set the volume for a currently playing sound instance, provided @@ -1070,7 +1097,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setVolume(handle, volume); + _controller.soLoudFFI.setVolume(handle, volume); } /// Get a sound's current pan setting. @@ -1089,7 +1116,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getPan(handle.id); + return SoLoudController().soLoudFFI.getPan(handle); } /// Set a sound's current pan setting. @@ -1112,7 +1139,7 @@ interface class SoLoud { pan >= -1 && pan <= 1, 'The pan argument must be in range -1 to 1 inclusive!', ); - return SoLoudController().soLoudFFI.setPan(handle.id, pan.clamp(-1, 1)); + return SoLoudController().soLoudFFI.setPan(handle, pan.clamp(-1, 1)); } /// Set the left/right volumes directly. @@ -1136,7 +1163,7 @@ interface class SoLoud { 'The panRight argument must be in range -1 to 1 inclusive!', ); return SoLoudController().soLoudFFI.setPanAbsolute( - handle.id, + handle, panLeft.clamp(-1, 1), panRight.clamp(-1, 1), ); @@ -1153,7 +1180,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getIsValidVoiceHandle(handle); + return _controller.soLoudFFI.getIsValidVoiceHandle(handle); } /// Returns the number of concurrent sounds that are playing at the moment. @@ -1161,7 +1188,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getActiveVoiceCount(); + return _controller.soLoudFFI.getActiveVoiceCount(); } /// Returns the number of concurrent sounds that are playing a @@ -1170,7 +1197,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.countAudioSource(audioSource.soundHash); + return _controller.soLoudFFI.countAudioSource(audioSource.soundHash); } /// Returns the number of voices the application has told SoLoud to play. @@ -1178,7 +1205,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getVoiceCount(); + return _controller.soLoudFFI.getVoiceCount(); } /// Get a sound's protection state. @@ -1188,7 +1215,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getProtectVoice(handle); + return _controller.soLoudFFI.getProtectVoice(handle); } /// Sets a sound instance's protection state. @@ -1218,7 +1245,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setProtectVoice(handle, protect); + _controller.soLoudFFI.setProtectVoice(handle, protect); } /// Gets the current maximum active voice count. @@ -1226,7 +1253,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getMaxActiveVoiceCount(); + return _controller.soLoudFFI.getMaxActiveVoiceCount(); } /// Sets the current maximum active voice count. @@ -1247,7 +1274,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setMaxActiveVoiceCount(maxVoiceCount); + _controller.soLoudFFI.setMaxActiveVoiceCount(maxVoiceCount); } /// Return a floats matrix of 256x512. @@ -1271,21 +1298,19 @@ interface class SoLoud { /// [GitHub](https://github.com/alnitak/flutter_soloud/issues) providing /// a simple working example. @experimental - void getAudioTexture2D(ffi.Pointer> audioData) { - if (!isInitialized || audioData == ffi.nullptr) { + @Deprecated('Please use AudioData class instead.') + void getAudioTexture2D(AudioData audioData) { + if (!isInitialized) { throw const SoLoudNotInitializedException(); } if (!_isVisualizationEnabled) { throw const SoLoudVisualizationNotEnabledException(); } - final error = SoLoudController().soLoudFFI.getAudioTexture2D(audioData); + final error = _controller.soLoudFFI.getAudioTexture2D(audioData); _logPlayerError(error, from: 'getAudioTexture2D() result'); if (error != PlayerErrors.noError) { throw SoLoudCppException.fromPlayerError(error); } - if (audioData.value == ffi.nullptr) { - throw const SoLoudNullPointerException(); - } } /// Smooth FFT data. @@ -1305,7 +1330,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - SoLoudController().soLoudFFI.setFftSmoothing(smooth); + _controller.soLoudFFI.setFftSmoothing(smooth); } // /////////////////////////////////////// @@ -1320,7 +1345,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.fadeGlobalVolume(to, time); + final ret = _controller.soLoudFFI.fadeGlobalVolume(to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'fadeGlobalVolume(): $error'); @@ -1338,7 +1363,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.fadeVolume(handle, to, time); + final ret = _controller.soLoudFFI.fadeVolume(handle, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'fadeVolume(): $error'); @@ -1356,7 +1381,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.fadePan(handle, to, time); + final ret = _controller.soLoudFFI.fadePan(handle, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'fadePan(): $error'); @@ -1374,8 +1399,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = - SoLoudController().soLoudFFI.fadeRelativePlaySpeed(handle, to, time); + final ret = _controller.soLoudFFI.fadeRelativePlaySpeed(handle, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'fadeRelativePlaySpeed(): $error'); @@ -1392,7 +1416,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.schedulePause(handle, time); + final ret = _controller.soLoudFFI.schedulePause(handle, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'schedulePause(): $error'); @@ -1409,7 +1433,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.scheduleStop(handle, time); + final ret = _controller.soLoudFFI.scheduleStop(handle, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'scheduleStop(): $error'); @@ -1431,8 +1455,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = - SoLoudController().soLoudFFI.oscillateVolume(handle, from, to, time); + final ret = _controller.soLoudFFI.oscillateVolume(handle, from, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'oscillateVolume(): $error'); @@ -1453,8 +1476,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = - SoLoudController().soLoudFFI.oscillatePan(handle, from, to, time); + final ret = _controller.soLoudFFI.oscillatePan(handle, from, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'oscillatePan(): $error'); @@ -1476,8 +1498,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController() - .soLoudFFI + final ret = _controller.soLoudFFI .oscillateRelativePlaySpeed(handle, from, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { @@ -1497,8 +1518,7 @@ interface class SoLoud { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - final ret = - SoLoudController().soLoudFFI.oscillateGlobalVolume(from, to, time); + final ret = _controller.soLoudFFI.oscillateGlobalVolume(from, to, time); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'oscillateGlobalVolume(): $error'); @@ -1515,7 +1535,7 @@ interface class SoLoud { /// Returns `-1` if the filter is not active. Otherwise, returns /// the index of the given filter. int isFilterActive(FilterType filterType) { - final ret = SoLoudController().soLoudFFI.isFilterActive(filterType.index); + final ret = _controller.soLoudFFI.isFilterActive(filterType); if (ret.error != PlayerErrors.noError) { _log.severe(() => 'isFilterActive(): ${ret.error}'); throw SoLoudCppException.fromPlayerError(ret.error); @@ -1529,8 +1549,7 @@ interface class SoLoud { /// /// Returns the list of param names. List getFilterParamNames(FilterType filterType) { - final ret = - SoLoudController().soLoudFFI.getFilterParamNames(filterType.index); + final ret = _controller.soLoudFFI.getFilterParamNames(filterType); if (ret.error != PlayerErrors.noError) { _log.severe(() => 'getFilterParamNames(): ${ret.error}'); throw SoLoudCppException.fromPlayerError(ret.error); @@ -1545,7 +1564,7 @@ interface class SoLoud { /// Throws [SoLoudFilterAlreadyAddedException] when trying to add a filter /// that has already been added. void addGlobalFilter(FilterType filterType) { - final e = SoLoudController().soLoudFFI.addGlobalFilter(filterType.index); + final e = _controller.soLoudFFI.addGlobalFilter(filterType); if (e != PlayerErrors.noError) { _log.severe(() => 'addGlobalFilter(): $e'); throw SoLoudCppException.fromPlayerError(e); @@ -1554,8 +1573,7 @@ interface class SoLoud { /// Removes [filterType] from all sounds. void removeGlobalFilter(FilterType filterType) { - final ret = - SoLoudController().soLoudFFI.removeGlobalFilter(filterType.index); + final ret = _controller.soLoudFFI.removeGlobalFilter(filterType); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'removeGlobalFilter(): $error'); @@ -1569,9 +1587,8 @@ interface class SoLoud { /// [getFilterParamNames]), and its new [value]. void setFilterParameter( FilterType filterType, int attributeId, double value) { - final ret = SoLoudController() - .soLoudFFI - .setFilterParams(filterType.index, attributeId, value); + final ret = + _controller.soLoudFFI.setFilterParams(filterType, attributeId, value); final error = PlayerErrors.values[ret]; if (error != PlayerErrors.noError) { _log.severe(() => 'setFxParams(): $error'); @@ -1586,9 +1603,7 @@ interface class SoLoud { /// /// Returns the value as [double]. double getFilterParameter(FilterType filterType, int attributeId) { - return SoLoudController() - .soLoudFFI - .getFilterParams(filterType.index, attributeId); + return _controller.soLoudFFI.getFilterParams(filterType, attributeId); } // //////////////////////////////////////////////// @@ -1644,19 +1659,19 @@ interface class SoLoud { throw const SoLoudNotInitializedException(); } - final ret = SoLoudController().soLoudFFI.play3d( - sound.soundHash, - posX, - posY, - posZ, - velX: velX, - velY: velY, - velZ: velZ, - volume: volume, - paused: paused, - looping: looping, - loopingStartAt: loopingStartAt, - ); + final ret = _controller.soLoudFFI.play3d( + sound.soundHash, + posX, + posY, + posZ, + velX: velX, + velY: velY, + velZ: velZ, + volume: volume, + paused: paused, + looping: looping, + loopingStartAt: loopingStartAt, + ); _logPlayerError(ret.error, from: 'play3d()'); if (ret.error != PlayerErrors.noError) { @@ -1684,14 +1699,14 @@ interface class SoLoud { /// that your world coordinates are in meters (where 1 unit is 1 meter), /// and that the environment is dry air at around 20 degrees Celsius. void set3dSoundSpeed(double speed) { - SoLoudController().soLoudFFI.set3dSoundSpeed(speed); + _controller.soLoudFFI.set3dSoundSpeed(speed); } /// Gets the speed of sound. /// /// See [set3dSoundSpeed] for details. double get3dSoundSpeed() { - return SoLoudController().soLoudFFI.get3dSoundSpeed(); + return _controller.soLoudFFI.get3dSoundSpeed(); } /// Sets the position, at-vector, up-vector and velocity @@ -1709,30 +1724,29 @@ interface class SoLoud { double velocityX, double velocityY, double velocityZ) { - SoLoudController().soLoudFFI.set3dListenerParameters(posX, posY, posZ, atX, - atY, atZ, upX, upY, upZ, velocityX, velocityY, velocityZ); + _controller.soLoudFFI.set3dListenerParameters(posX, posY, posZ, atX, atY, + atZ, upX, upY, upZ, velocityX, velocityY, velocityZ); } /// Sets the position parameter of the 3D audio listener. void set3dListenerPosition(double posX, double posY, double posZ) { - SoLoudController().soLoudFFI.set3dListenerPosition(posX, posY, posZ); + _controller.soLoudFFI.set3dListenerPosition(posX, posY, posZ); } /// Sets the at-vector (i.e. position) parameter of the 3D audio listener. void set3dListenerAt(double atX, double atY, double atZ) { - SoLoudController().soLoudFFI.set3dListenerAt(atX, atY, atZ); + _controller.soLoudFFI.set3dListenerAt(atX, atY, atZ); } /// Sets the up-vector parameter of the 3D audio listener. void set3dListenerUp(double upX, double upY, double upZ) { - SoLoudController().soLoudFFI.set3dListenerUp(upX, upY, upZ); + _controller.soLoudFFI.set3dListenerUp(upX, upY, upZ); } /// Sets the 3D listener's velocity vector. void set3dListenerVelocity( double velocityX, double velocityY, double velocityZ) { - SoLoudController() - .soLoudFFI + _controller.soLoudFFI .set3dListenerVelocity(velocityX, velocityY, velocityZ); } @@ -1742,21 +1756,20 @@ interface class SoLoud { /// The sound instance is provided via its [handle]. void set3dSourceParameters(SoundHandle handle, double posX, double posY, double posZ, double velocityX, double velocityY, double velocityZ) { - SoLoudController().soLoudFFI.set3dSourceParameters( + _controller.soLoudFFI.set3dSourceParameters( handle, posX, posY, posZ, velocityX, velocityY, velocityZ); } /// Sets the position of a live 3D audio source. void set3dSourcePosition( SoundHandle handle, double posX, double posY, double posZ) { - SoLoudController().soLoudFFI.set3dSourcePosition(handle, posX, posY, posZ); + _controller.soLoudFFI.set3dSourcePosition(handle, posX, posY, posZ); } /// Set the velocity parameter of a live 3D audio source. void set3dSourceVelocity(SoundHandle handle, double velocityX, double velocityY, double velocityZ) { - SoLoudController() - .soLoudFFI + _controller.soLoudFFI .set3dSourceVelocity(handle, velocityX, velocityY, velocityZ); } @@ -1764,8 +1777,7 @@ interface class SoLoud { /// of a live 3D audio source. void set3dSourceMinMaxDistance( SoundHandle handle, double minDistance, double maxDistance) { - SoLoudController() - .soLoudFFI + _controller.soLoudFFI .set3dSourceMinMaxDistance(handle, minDistance, maxDistance); } @@ -1785,18 +1797,16 @@ interface class SoLoud { int attenuationModel, double attenuationRolloffFactor, ) { - SoLoudController().soLoudFFI.set3dSourceAttenuation( - handle, - attenuationModel, - attenuationRolloffFactor, - ); + _controller.soLoudFFI.set3dSourceAttenuation( + handle, + attenuationModel, + attenuationRolloffFactor, + ); } /// Sets the doppler factor of a live 3D audio source. void set3dSourceDopplerFactor(SoundHandle handle, double dopplerFactor) { - SoLoudController() - .soLoudFFI - .set3dSourceDopplerFactor(handle, dopplerFactor); + _controller.soLoudFFI.set3dSourceDopplerFactor(handle, dopplerFactor); } /// Utility method that logs a [Level.SEVERE] message if [playerError] diff --git a/lib/src/soloud_capture.dart b/lib/src/soloud_capture.dart index 7a2f333..5d7f9c0 100644 --- a/lib/src/soloud_capture.dart +++ b/lib/src/soloud_capture.dart @@ -1,8 +1,10 @@ -import 'dart:ffi' as ffi; +// import 'dart:ffi' as ffi; +import 'package:flutter_soloud/src/bindings/audio_data.dart'; +import 'package:flutter_soloud/src/bindings/soloud_controller.dart'; import 'package:flutter_soloud/src/enums.dart'; +import 'package:flutter_soloud/src/exceptions/exceptions.dart'; import 'package:flutter_soloud/src/soloud.dart'; -import 'package:flutter_soloud/src/soloud_controller.dart'; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; @@ -75,55 +77,28 @@ interface class SoLoudCapture { // Below all the methods implemented with FFI for the capture // //////////////////////////////////////////////// - /// Return a floats matrix of 256x512 - /// Every row are composed of 256 FFT values plus 256 of wave data - /// Every time is called, a new row is stored in the - /// first row and all the previous rows are shifted - /// up (the last one will be lost). - /// - /// Return [CaptureErrors.captureNoError] if no error. - /// - CaptureErrors getCaptureAudioTexture2D( - ffi.Pointer> audioData, - ) { - if (!isCaptureInited || audioData == ffi.nullptr) { - _log.severe( - () => 'getCaptureAudioTexture2D(): ${CaptureErrors.captureNotInited}', - ); - return CaptureErrors.captureNotInited; - } - - final ret = - SoLoudController().captureFFI.getCaptureAudioTexture2D(audioData); - _logCaptureError(ret, from: 'getCaptureAudioTexture2D() result'); - - if (ret != CaptureErrors.captureNoError) { - return ret; - } - if (audioData.value == ffi.nullptr) { - _logCaptureError( - CaptureErrors.nullPointer, - from: 'getCaptureAudioTexture2D() result', - ); - return CaptureErrors.nullPointer; - } - return CaptureErrors.captureNoError; - } - /// Initialize input device with [deviceID]. /// /// Return [CaptureErrors.captureNoError] if no error. /// - CaptureErrors initialize({int deviceID = -1}) { + CaptureErrors init({int deviceID = -1}) { final ret = SoLoudController().captureFFI.initCapture(deviceID); _logCaptureError(ret, from: 'initCapture() result'); if (ret == CaptureErrors.captureNoError) { isCaptureInited = true; + } else { + throw SoLoudCppException.fromCaptureError(ret); } return ret; } + /// Dispose capture device. + void deinit() { + SoLoudController().captureFFI.disposeCapture(); + isCaptureInited = false; + } + /// Get the status of the device. /// bool isCaptureInitialized() { @@ -162,6 +137,33 @@ interface class SoLoudCapture { return ret; } + /// Return a floats matrix of 256x512 + /// Every row are composed of 256 FFT values plus 256 of wave data + /// Every time is called, a new row is stored in the + /// first row and all the previous rows are shifted + /// up (the last one will be lost). + /// + /// Return [CaptureErrors.captureNoError] if no error. + /// + @Deprecated('Please use AudioData class instead.') + CaptureErrors getCaptureAudioTexture2D(AudioData audioData) { + if (!isCaptureInited) { + _log.severe( + () => 'getCaptureAudioTexture2D(): ${CaptureErrors.captureNotInited}', + ); + return CaptureErrors.captureNotInited; + } + + final ret = + SoLoudController().captureFFI.getCaptureAudioTexture2D(audioData); + _logCaptureError(ret, from: 'getCaptureAudioTexture2D() result'); + + if (ret != CaptureErrors.captureNoError) { + return ret; + } + return CaptureErrors.captureNoError; + } + /// Start capturing audio data. /// /// Return [CaptureErrors.captureNoError] if no error @@ -169,6 +171,9 @@ interface class SoLoudCapture { CaptureErrors startCapture() { final ret = SoLoudController().captureFFI.startCapture(); _logCaptureError(ret, from: 'startCapture() result'); + if (ret != CaptureErrors.captureNoError) { + throw SoLoudCppException.fromCaptureError(ret); + } return ret; } @@ -181,6 +186,8 @@ interface class SoLoudCapture { _logCaptureError(ret, from: 'stopCapture() result'); if (ret == CaptureErrors.captureNoError) { isCaptureInited = false; + } else { + throw SoLoudCppException.fromCaptureError(ret); } return ret; } diff --git a/lib/src/sound_hash.dart b/lib/src/sound_hash.dart index 2659d3a..de3b5d2 100644 --- a/lib/src/sound_hash.dart +++ b/lib/src/sound_hash.dart @@ -37,7 +37,8 @@ extension type SoundHash._(int hash) { @internal factory SoundHash.random() { // Dart must support 32 bit systems. - const largest32BitInt = 1 << 32; + // Shifting by 31 because the leftmost bit is used for the sign. + const largest32BitInt = 1 << 31; // Generate a random integer, but not 0 (which is reserved for invalid // sound hashes). final soundHash = _random.nextInt(largest32BitInt - 1) + 1; diff --git a/lib/src/utils/loader.dart b/lib/src/utils/loader.dart index 23b19b8..1066f85 100644 --- a/lib/src/utils/loader.dart +++ b/lib/src/utils/loader.dart @@ -1,303 +1,3 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_soloud/src/exceptions/exceptions.dart'; -import 'package:http/http.dart' as http; -import 'package:logging/logging.dart'; -import 'package:meta/meta.dart'; -import 'package:path/path.dart' as path; -import 'package:path_provider/path_provider.dart' as path_provider; - -/// A long-living helper class that loads assets and URLs into temporary files -/// that can be played by SoLoud. -@internal -class SoLoudLoader { - SoLoudLoader(); - - static const String _temporaryDirectoryName = 'SoLoudLoader-Temp-Files'; - - static final Logger _log = Logger('flutter_soloud.SoLoudLoader'); - - /// When `true`, the loader will automatically call [cleanUp], - /// deleting files in the temporary directory at various points. - bool automaticCleanup = false; - - final Map<_TemporaryFileIdentifier, File> _temporaryFiles = {}; - - Directory? _temporaryDirectory; - - /// Deletes temporary files. It is good practice to call this method - /// once in a while, in order not to bloat the temporary directory. - /// This is especially important when the application 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. - Future cleanUp() async { - _log.finest('cleanUp() called'); - final directory = _temporaryDirectory; - - if (directory == null) { - _log.warning("Temporary directory hasn't been initialized, " - 'yet cleanUp() is called.'); - return; - } - - // Clear the set in case someone tries to access the files - // while we're deleting them. - _temporaryFiles.clear(); - - try { - final files = await directory.list(followLinks: false).toList(); - - Future deleteFile(FileSystemEntity entity) async { - try { - await entity.delete(recursive: true); - } on FileSystemException catch (e) { - _log.warning(() => 'cleanUp() cannot remove ${entity.path}: $e'); - return false; - } - return true; - } - - // Delete files in parallel. - final results = await Future.wait(files.map(deleteFile)); - - if (results.any((success) => !success)) { - _log.severe('Cannot clean up temporary directory. See warnings above.'); - } - - await _temporaryDirectory?.delete(recursive: true); - } on FileSystemException catch (e) { - _log.severe('Cannot clean up temporary directory: $e'); - } - } - - /// This method can be run safely several times. - Future initialize() async { - _log.finest('initialize() called'); - if (_temporaryDirectory != null) { - _log.fine( - () => 'Loader has already been initialized. Not initializing again.', - ); - if (automaticCleanup) { - await cleanUp(); - } - return; - } - - final systemTempDir = await path_provider.getTemporaryDirectory(); - final directoryPath = - path.join(systemTempDir.path, _temporaryDirectoryName); - final directory = Directory(directoryPath); - - try { - _temporaryDirectory = await directory.create(); - } catch (e) { - _log.severe( - "Couldn't initialize loader. Temporary directory couldn't be created.", - e, - ); - // There is no way we can recover from this. If we have nowhere to save - // files, we can't play anything. - rethrow; - } - - _log.info(() => 'Temporary directory initialized at ${directory.path}'); - - if (automaticCleanup) { - await cleanUp(); - } - } - - /// Loads the asset with [key] (e.g. `assets/sound.mp3`), and creates - /// a temporary file that can be played by SoLoud. - /// - /// Provide [assetBundle] if needed. By default, the method uses - /// [rootBundle]. - /// - /// Returns `null` if there's a problem with some implementation detail - /// (e.g. cannot create temporary file). - /// - /// Throws [FlutterError] when the asset doesn't exist or cannot be loaded. - /// (This is the same exception that [AssetBundle.load] would throw.) - Future loadAsset( - String key, { - AssetBundle? assetBundle, - }) async { - final id = _TemporaryFileIdentifier(_Source.asset, key); - - // TODO(filiph): Add the option to check the filesystem first. - // This could be a cache-invalidation problem (if the asset - // changes from one version to another). But it could speed - // up start up times. - if (_temporaryFiles.containsKey(id)) { - final existingFile = _temporaryFiles[id]!; - if (existingFile.existsSync()) { - _log.finest(() => 'Asset $key already exists as a temporary file.'); - return _temporaryFiles[id]!; - } - } - - final directory = _temporaryDirectory; - - if (directory == null) { - throw const SoLoudTemporaryFolderFailedException( - "Temporary directory hasn't been initialized, " - 'yet loadAsset() is called.'); - } - - final newFilepath = _getFullTempFilePath(id); - final newFile = File(newFilepath); - - final ByteData byteData; - final bundle = assetBundle ?? rootBundle; - - try { - byteData = await bundle.load(key); - } catch (e) { - _log.severe("loadAsset() couldn't load $key from $bundle", e); - // Fail-fast principle. If the developer tries to load an asset - // that's not available, this should be seen during debugging, - // and the developer should be able to catch and deal with the error - // in a try-catch block. - rethrow; - } - - final buffer = byteData.buffer; - try { - await newFile.create(recursive: true); - await newFile.writeAsBytes( - buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), - ); - } catch (e) { - throw SoLoudTemporaryFolderFailedException( - "loadAsset() couldn't write $newFile to disk", - ); - } - - _temporaryFiles[id] = newFile; - - return newFile; - } - - /// 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, this method - /// will create a new one and close it afterwards. - /// - /// Throws [FormatException] if the [url] is invalid. - /// Throws [SoLoudNetworkStatusCodeException] if the request fails. - Future loadUrl( - String url, { - http.Client? httpClient, - }) async { - final uri = Uri.parse(url); - - final id = _TemporaryFileIdentifier(_Source.url, url); - - // TODO(filiph): Add the option to check the filesystem first. - // This could be a cache-invalidation problem (if the asset - // changes from one version to another). But it could speed - // up start up times. - if (_temporaryFiles.containsKey(id)) { - final existingFile = _temporaryFiles[id]!; - if (existingFile.existsSync()) { - _log.finest( - () => 'Sound from $url already exists as a temporary file.', - ); - return _temporaryFiles[id]!; - } - } - - final directory = _temporaryDirectory; - - if (directory == null) { - throw const SoLoudTemporaryFolderFailedException( - "Temporary directory hasn't been initialized, " - 'yet loadUrl() is called.'); - } - - final newFilepath = _getFullTempFilePath(id); - final newFile = File(newFilepath); - - final Uint8List byteData; - - try { - http.Response response; - if (httpClient != null) { - response = await httpClient.get(uri); - } else { - response = await http.get(uri); - } - if (response.statusCode == 200) { - byteData = response.bodyBytes; - } else { - throw SoLoudNetworkStatusCodeException( - response.statusCode, - 'Failed to fetch file from URL: $url', - ); - } - } catch (e) { - _log.severe(() => 'Error fetching $url', e); - rethrow; - } - - final buffer = byteData.buffer; - try { - await newFile.create(recursive: true); - await newFile.writeAsBytes( - buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), - ); - } catch (e) { - throw SoLoudTemporaryFolderFailedException( - "loadAsset() couldn't write $newFile to disk", - ); - } - - _temporaryFiles[id] = newFile; - - return newFile; - } - - String _getFullTempFilePath(_TemporaryFileIdentifier id) { - final directory = _temporaryDirectory; - - if (directory == null) { - throw StateError("Temporary directory hasn't been initialized, " - 'yet _getTempFile() is called.'); - } - - return path.join(directory.absolute.path, id.asFilename); - } -} - -enum _Source { - url, - asset, -} - -@immutable -class _TemporaryFileIdentifier { - const _TemporaryFileIdentifier(this.source, this.path); - - final _Source source; - - final String path; - - String get asFilename => - 'temp-sound-${source.name}-0x${path.hashCode.toRadixString(16)}'; - - @override - int get hashCode => Object.hash(source, path); - - @override - bool operator ==(Object other) { - return other is _TemporaryFileIdentifier && - other.source == source && - other.path == path; - } -} +export 'loader_base.dart' + if (dart.library.io) 'loader_io.dart' + if (dart.library.js_interop) 'loader_web.dart'; diff --git a/lib/src/utils/loader_base.dart b/lib/src/utils/loader_base.dart new file mode 100644 index 0000000..45c899e --- /dev/null +++ b/lib/src/utils/loader_base.dart @@ -0,0 +1,29 @@ +// ignore_for_file: public_member_api_docs + +import 'package:flutter/services.dart'; +import 'package:flutter_soloud/src/audio_source.dart'; +import 'package:flutter_soloud/src/enums.dart'; +import 'package:http/http.dart' as http; + +/// Stub interface for unsupported plarforms. +interface class SoLoudLoader { + /// To reflect [SoLoudLoader] for `io`. + bool automaticCleanup = false; + + Future initialize() async => + throw UnsupportedError('platform not supported'); + + Future loadAsset( + String key, + LoadMode mode, { + AssetBundle? assetBundle, + }) async => + throw UnsupportedError('platform not supported'); + + Future loadUrl( + String url, + LoadMode mode, { + http.Client? httpClient, + }) async => + throw UnsupportedError('platform not supported'); +} diff --git a/lib/src/utils/loader_io.dart b/lib/src/utils/loader_io.dart new file mode 100644 index 0000000..608cc9a --- /dev/null +++ b/lib/src/utils/loader_io.dart @@ -0,0 +1,320 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.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/soloud.dart'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart' as path_provider; + +/// A long-living helper class that loads assets and URLs into temporary files +/// that can be played by SoLoud. +@internal +class SoLoudLoader { + SoLoudLoader(); + + static const String _temporaryDirectoryName = 'SoLoudLoader-Temp-Files'; + + static final Logger _log = Logger('flutter_soloud.SoLoudLoader_io'); + + /// When `true`, the loader will automatically call [cleanUp], + /// deleting files in the temporary directory at various points. + bool automaticCleanup = false; + + final Map<_TemporaryFileIdentifier, File> _temporaryFiles = {}; + + Directory? _temporaryDirectory; + + /// Deletes temporary files. It is good practice to call this method + /// once in a while, in order not to bloat the temporary directory. + /// This is especially important when the application 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. + Future cleanUp() async { + _log.finest('cleanUp() called'); + final directory = _temporaryDirectory; + + if (directory == null) { + _log.warning("Temporary directory hasn't been initialized, " + 'yet cleanUp() is called.'); + return; + } + + // Clear the set in case someone tries to access the files + // while we're deleting them. + _temporaryFiles.clear(); + + try { + final files = await directory.list(followLinks: false).toList(); + + Future deleteFile(FileSystemEntity entity) async { + try { + await entity.delete(recursive: true); + } on FileSystemException catch (e) { + _log.warning(() => 'cleanUp() cannot remove ${entity.path}: $e'); + return false; + } + return true; + } + + // Delete files in parallel. + final results = await Future.wait(files.map(deleteFile)); + + if (results.any((success) => !success)) { + _log.severe('Cannot clean up temporary directory. See warnings above.'); + } + + await _temporaryDirectory?.delete(recursive: true); + } on FileSystemException catch (e) { + _log.severe('Cannot clean up temporary directory: $e'); + } + } + + /// This method can be run safely several times. + Future initialize() async { + _log.finest('initialize() called'); + if (_temporaryDirectory != null) { + _log.fine( + () => 'Loader has already been initialized. Not initializing again.', + ); + if (automaticCleanup) { + await cleanUp(); + } + return; + } + + final systemTempDir = await path_provider.getTemporaryDirectory(); + final directoryPath = + path.join(systemTempDir.path, _temporaryDirectoryName); + final directory = Directory(directoryPath); + + try { + _temporaryDirectory = await directory.create(); + } catch (e) { + _log.severe( + "Couldn't initialize loader. Temporary directory couldn't be created.", + e, + ); + // There is no way we can recover from this. If we have nowhere to save + // files, we can't play anything. + rethrow; + } + + _log.info(() => 'Temporary directory initialized at ${directory.path}'); + + if (automaticCleanup) { + await cleanUp(); + } + } + + /// Loads the asset with [key] (e.g. `assets/sound.mp3`), and creates + /// a temporary file that can be played by SoLoud. + /// + /// Provide [assetBundle] if needed. By default, the method uses + /// [rootBundle]. + /// + /// Returns `null` if there's a problem with some implementation detail + /// (e.g. cannot create temporary file). + /// + /// Throws [FlutterError] when the asset doesn't exist or cannot be loaded. + /// (This is the same exception that [AssetBundle.load] would throw.) + Future loadAsset( + String key, + LoadMode mode, { + AssetBundle? assetBundle, + }) async { + final id = _TemporaryFileIdentifier(_Source.asset, key); + + // TODO(filiph): Add the option to check the filesystem first. + // This could be a cache-invalidation problem (if the asset + // changes from one version to another). But it could speed + // up start up times. + if (_temporaryFiles.containsKey(id)) { + final existingFile = _temporaryFiles[id]!; + if (existingFile.existsSync()) { + _log.finest(() => 'Asset $key already exists as a temporary file.'); + final audioSource = + SoLoud.instance.loadFile(existingFile.path, mode: mode); + return audioSource; + } + } + + final directory = _temporaryDirectory; + + if (directory == null) { + throw const SoLoudTemporaryFolderFailedException( + "Temporary directory hasn't been initialized, " + 'yet loadAsset() is called.'); + } + + final newFilepath = _getFullTempFilePath(id); + final newFile = File(newFilepath); + + final ByteData byteData; + final bundle = assetBundle ?? rootBundle; + + try { + byteData = await bundle.load(key); + } catch (e) { + _log.severe("loadAsset() couldn't load $key from $bundle", e); + // Fail-fast principle. If the developer tries to load an asset + // that's not available, this should be seen during debugging, + // and the developer should be able to catch and deal with the error + // in a try-catch block. + rethrow; + } + + final buffer = byteData.buffer; + try { + await newFile.create(recursive: true); + await newFile.writeAsBytes( + buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), + ); + } catch (e) { + throw SoLoudTemporaryFolderFailedException( + "loadAsset() couldn't write $newFile to disk", + ); + } + + _temporaryFiles[id] = newFile; + + final audioSource = SoLoud.instance.loadFile(newFile.path, mode: mode); + return audioSource; + } + + /// 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, this method + /// will create a new one and close it afterwards. + /// + /// Throws [FormatException] if the [url] is invalid. + /// Throws [SoLoudNetworkStatusCodeException] if the request fails. + Future loadUrl( + String url, + LoadMode mode, { + http.Client? httpClient, + }) async { + final uri = Uri.parse(url); + + final id = _TemporaryFileIdentifier(_Source.url, url); + + // TODO(filiph): Add the option to check the filesystem first. + // This could be a cache-invalidation problem (if the asset + // changes from one version to another). But it could speed + // up start up times. + if (_temporaryFiles.containsKey(id)) { + final existingFile = _temporaryFiles[id]!; + if (existingFile.existsSync()) { + _log.finest( + () => 'Sound from $url already exists as a temporary file.', + ); + final newAudioSource = await SoLoud.instance.loadFile( + existingFile.path, + mode: mode, + ); + return newAudioSource; + } + } + + final directory = _temporaryDirectory; + + if (directory == null) { + throw const SoLoudTemporaryFolderFailedException( + "Temporary directory hasn't been initialized, " + 'yet loadUrl() is called.'); + } + + final newFilepath = _getFullTempFilePath(id); + final newFile = File(newFilepath); + + final Uint8List byteData; + + try { + http.Response response; + if (httpClient != null) { + response = await httpClient.get(uri); + } else { + response = await http.get(uri); + } + if (response.statusCode == 200) { + byteData = response.bodyBytes; + } else { + throw SoLoudNetworkStatusCodeException( + response.statusCode, + 'Failed to fetch file from URL: $url', + ); + } + } catch (e) { + _log.severe(() => 'Error fetching $url', e); + rethrow; + } + + final buffer = byteData.buffer; + try { + await newFile.create(recursive: true); + await newFile.writeAsBytes( + buffer.asUint8List(byteData.offsetInBytes, byteData.lengthInBytes), + ); + } catch (e) { + throw SoLoudTemporaryFolderFailedException( + "loadAsset() couldn't write $newFile to disk", + ); + } + + _temporaryFiles[id] = newFile; + + final newAudioSource = await SoLoud.instance.loadFile( + newFile.path, + mode: mode, + ); + + return newAudioSource; + } + + String _getFullTempFilePath(_TemporaryFileIdentifier id) { + final directory = _temporaryDirectory; + + if (directory == null) { + throw StateError("Temporary directory hasn't been initialized, " + 'yet _getTempFile() is called.'); + } + + return path.join(directory.absolute.path, id.asFilename); + } +} + +enum _Source { + url, + asset, +} + +@immutable +class _TemporaryFileIdentifier { + const _TemporaryFileIdentifier(this.source, this.path); + + final _Source source; + + final String path; + + String get asFilename => + 'temp-sound-${source.name}-0x${path.hashCode.toRadixString(16)}'; + + @override + int get hashCode => Object.hash(source, path); + + @override + bool operator ==(Object other) { + return other is _TemporaryFileIdentifier && + other.source == source && + other.path == path; + } +} diff --git a/lib/src/utils/loader_web.dart b/lib/src/utils/loader_web.dart new file mode 100644 index 0000000..a6f9405 --- /dev/null +++ b/lib/src/utils/loader_web.dart @@ -0,0 +1,103 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.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/soloud.dart'; +import 'package:http/http.dart' as http; +import 'package:logging/logging.dart'; +import 'package:meta/meta.dart'; + +/// A long-living helper class that loads assets and URLs into temporary files +/// that can be played by SoLoud. +@internal +class SoLoudLoader { + SoLoudLoader(); + + static final Logger _log = Logger('flutter_soloud.SoLoudLoader_web'); + + /// To reflect [SoLoudLoader] for `io`. + bool automaticCleanup = false; + + /// To reflect [SoLoudLoader] for `io`. + Future initialize() async {} + + /// Loads the asset with [key] (e.g. `assets/sound.mp3`), and return + /// the file byte as `Uint8List` to be passed to `SoLoud.LoadMem` for + /// the web platfom. + /// + /// Provide [assetBundle] if needed. By default, the method uses + /// [rootBundle]. + /// + /// Returns `null` if there's a problem with some implementation detail + /// (e.g. cannot create temporary file). + /// + /// Throws [FlutterError] when the asset doesn't exist or cannot be loaded. + /// (This is the same exception that [AssetBundle.load] would throw.) + Future loadAsset( + String key, + LoadMode mode, { + AssetBundle? assetBundle, + }) async { + final ByteData byteData; + final bundle = assetBundle ?? rootBundle; + + try { + byteData = await bundle.load(key); + } catch (e) { + _log.severe("loadAsset() couldn't load $key from $bundle", e); + // Fail-fast principle. If the developer tries to load an asset + // that's not available, this should be seen during debugging, + // and the developer should be able to catch and deal with the error + // in a try-catch block. + rethrow; + } + + final buffer = byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes); + final newAudioSource = SoLoud.instance.loadMem(key, buffer, mode: mode); + return newAudioSource; + } + + /// 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, this method + /// will create a new one and close it afterwards. + /// + /// Throws [FormatException] if the [url] is invalid. + /// Throws [SoLoudNetworkStatusCodeException] if the request fails. + Future loadUrl( + String url, + LoadMode mode, { + http.Client? httpClient, + }) async { + final uri = Uri.parse(url); + + final Uint8List byteData; + + try { + http.Response response; + if (httpClient != null) { + response = await httpClient.get(uri); + } else { + response = await http.get(uri); + } + if (response.statusCode == 200) { + byteData = response.bodyBytes; + } else { + throw SoLoudNetworkStatusCodeException( + response.statusCode, + 'Failed to fetch file from URL: $url', + ); + } + } catch (e) { + _log.severe(() => 'Error fetching $url', e); + rethrow; + } + + final buffer = byteData.buffer + .asUint8List(byteData.offsetInBytes, byteData.lengthInBytes); + final newAudioSource = SoLoud.instance.loadMem(url, buffer); + return newAudioSource; + } +} diff --git a/lib/src/worker/worker.dart b/lib/src/worker/worker.dart new file mode 100644 index 0000000..20704d5 --- /dev/null +++ b/lib/src/worker/worker.dart @@ -0,0 +1,69 @@ +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert' show jsonEncode; +import 'dart:js_interop'; + +import 'package:meta/meta.dart'; +import 'package:web/web.dart' as web; + +@internal +class WorkerController { + web.Worker? _worker; + StreamController? _outputController; + + /// Spawn a new web Worker with the given JS source (not used now). + static Future spawn(String path) async { + final controller = WorkerController() + .._outputController = StreamController() + .._worker = web.Worker(path.endsWith('.dart') ? '$path.js' : path); + + controller._worker?.onmessage = ((web.MessageEvent event) { + controller._outputController?.add(event.data.dartify()); + }).toJS; + + return controller; + } + + /// Set the worker created in WASM. + /// This is used to get events sent from the native audio thread. + void setWasmWorker(web.Worker wasmWorker) { + _outputController = StreamController(); + _worker = wasmWorker; + _worker?.onmessage = ((web.MessageEvent event) { + _outputController?.add(event.data.dartify()); + }).toJS; + } + + /// Not used with `Module.wasmWorker`. + void sendMessage(dynamic message) { + switch (message) { + case Map(): + final mapEncoded = jsonEncode(message); + _worker?.postMessage(mapEncoded.jsify()); + case num(): + _worker?.postMessage(message.toJS); + case String(): + _worker?.postMessage(message.toJS); + default: + try { + final messageJsifyed = (message as Object).jsify(); + _worker?.postMessage(messageJsifyed); + } catch (e) { + throw UnsupportedError( + 'sendMessage(): Type ${message.runtimeType} unsupported', + ); + } + } + } + + /// The receiver Stream. + Stream onReceive() { + return _outputController!.stream; + } + + /// Kill the Worker. + void kill() { + _worker?.terminate(); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 7ae68ab..4dc57d1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -27,15 +27,16 @@ environment: flutter: '>=3.3.0' dependencies: - ffi: ^2.0.2 + ffi: ^2.1.2 flutter: sdk: flutter - http: ^1.1.0 - logging: ^1.0.0 + http: ^1.2.1 + logging: ^1.2.0 meta: ^1.0.0 - path: ^1.8.1 - path_provider: ^2.0.15 - plugin_platform_interface: ^2.1.5 + path: ^1.9.0 + path_provider: ^2.1.3 + plugin_platform_interface: ^2.0.2 + web: ^0.5.1 dev_dependencies: ffigen: ^12.0.0 @@ -57,3 +58,12 @@ flutter: ffiPlugin: true windows: ffiPlugin: true + + assets: + # These assets are only needed for the web platform. + # Waiting for https://github.com/flutter/flutter/issues/65065 and + # https://github.com/flutter/flutter/issues/8230 to be addressed + # to make a conditional build. + - web/worker.dart.js + - web/libflutter_soloud_plugin.js + - web/libflutter_soloud_plugin.wasm diff --git a/src/bindings.cpp b/src/bindings.cpp index b8157c5..7618abb 100644 --- a/src/bindings.cpp +++ b/src/bindings.cpp @@ -6,6 +6,10 @@ #include "common.h" #endif +#ifdef __EMSCRIPTEN__ +#include +#endif + #include "soloud/include/soloud_fft.h" #include "soloud_thread.h" @@ -31,6 +35,51 @@ extern "C" void (*dartFileLoadedCallback)(enum PlayerErrors *, char *completeFileName, unsigned int *) = nullptr; void (*dartStateChangedCallback)(enum PlayerStateEvents *) = nullptr; + ////////////////////////////////////////////////////////////// + /// WEB WORKER + +#ifdef __EMSCRIPTEN__ + /// Create the web worker and store a global "Module.workerUri" in JS. + FFI_PLUGIN_EXPORT void createWorkerInWasm() + { + printf("CPP void createWorkerInWasm()\n"); + + EM_ASM({ + if (!Module.wasmWorker) + { + // Create a new Worker from the URI + var workerUri = "assets/packages/flutter_soloud/web/worker.dart.js"; + console.log("EM_ASM creating web worker!"); + Module.wasmWorker = new Worker(workerUri); + } + else + { + console.log("EM_ASM web worker already created!"); + } + }); + } + + /// Post a message with the web worker. + FFI_PLUGIN_EXPORT void sendToWorker(const char *message, int value) + { + EM_ASM({ + if (Module.wasmWorker) + { + console.log("EM_ASM posting message " + UTF8ToString($0) + + " with value " + $1); + // Send the message + Module.wasmWorker.postMessage(JSON.stringify({ + "message" : UTF8ToString($0), + "value" : $1 + })); + } + else + { + console.error('Worker not found.'); + } }, message, value); + } +#endif + FFI_PLUGIN_EXPORT void nativeFree(void *pointer) { free(pointer); @@ -38,9 +87,17 @@ extern "C" /// The callback to monitor when a voice ends. /// - /// It is called by void `Soloud::stopVoice_internal(unsigned int aVoice)` when a voice ends. - void voiceEndedCallback(unsigned int *handle) + /// It is called by void `Soloud::stopVoice_internal(unsigned int aVoice)` when a voice ends + /// and comes from the audio thread (so on the web, from a different web worker). + FFI_PLUGIN_EXPORT void voiceEndedCallback(unsigned int *handle) { +#ifdef __EMSCRIPTEN__ + // Calling JavaScript from C/C++ + // https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-call-javascript-from-native + // emscripten_run_script("voiceEndedCallbackJS('1234')"); + sendToWorker("voiceEndedCallback", *handle); +#endif + if (dartVoiceEndedCallback == nullptr) return; player->removeHandle(*handle); @@ -182,17 +239,25 @@ extern "C" /// [uniqueName] the unique name of the sound. Used only to have the [hash]. /// [buffer] the audio data. These contains the audio file bytes. /// [length] the length of [buffer]. + /// [loadIntoMem] if true Soloud::wav will be used which loads + /// all audio data into memory. This will be useful when + /// the audio is short, ie for game sounds, mainly used to prevent + /// gaps or lags when starting a sound (less CPU, more memory allocated). + /// If false, Soloud::wavStream will be used and the audio data is loaded + /// from the given file when needed (more CPU, less memory allocated). + /// See the [seek] note problem when using [loadIntoMem] = false /// [hash] return the hash of the sound. FFI_PLUGIN_EXPORT enum PlayerErrors loadMem( char *uniqueName, unsigned char *buffer, int length, + int loadIntoMem, unsigned int *hash) { // this check is already been done in Dart if (player.get() == nullptr || !player.get()->isInited()) return backendNotInited; - return (PlayerErrors)player.get()->loadMem(uniqueName, buffer, length, *hash); + return (PlayerErrors)player.get()->loadMem(uniqueName, buffer, length, loadIntoMem, *hash); } /// Load a new waveform to be played once or multiple times later @@ -395,7 +460,8 @@ extern "C" { if (player.get() == nullptr || !player.get()->isInited()) return backendNotInited; - *handle = player.get()->play(soundHash, volume, pan, paused, looping, loopingStartAt); + unsigned int newHandle = player.get()->play(soundHash, volume, pan, paused, looping, loopingStartAt); + *handle = newHandle; return *handle == 0 ? soundHashNotFound : noError; } @@ -500,24 +566,22 @@ extern "C" /// Returns valid data only if VisualizationEnabled is true /// - /// [fft] /// Return a 256 float array containing FFT data. - FFI_PLUGIN_EXPORT void getFft(float *fft) + FFI_PLUGIN_EXPORT void getFft(float **fft) { if (player.get() == nullptr || !player.get()->isInited()) return; - fft = player.get()->calcFFT(); + *fft = player.get()->calcFFT(); } /// Returns valid data only if VisualizationEnabled is true /// - /// fft /// Return a 256 float array containing wave data. - FFI_PLUGIN_EXPORT void getWave(float *wave) + FFI_PLUGIN_EXPORT void getWave(float **wave) { if (player.get() == nullptr || !player.get()->isInited()) return; - wave = player.get()->getWave(); + *wave = player.get()->getWave(); } /// Smooth FFT data. @@ -557,22 +621,21 @@ extern "C" memcpy(samples + 256, wave, sizeof(float) * 256); } - /// Return a floats matrix of 512x256 + /// Return a floats matrix of 256x512 /// Every row are composed of 256 FFT values plus 256 of wave data /// Every time is called, a new row is stored in the /// first row and all the previous rows are shifted /// up (the last one will be lost). /// /// [samples] - float texture2D[512][256]; + float texture2D[256][512]; FFI_PLUGIN_EXPORT enum PlayerErrors getAudioTexture2D(float **samples) { if (player.get() == nullptr || !player.get()->isInited() || analyzer.get() == nullptr || !player.get()->isVisualizationEnabled()) { - if (*samples == nullptr) - return unknownError; - memset(samples, 0, sizeof(float) * 512 * 256); + *samples = *texture2D; + memset(*samples, 0, sizeof(float) * 512 * 256); return backendNotInited; } /// shift up 1 row @@ -583,6 +646,10 @@ extern "C" return noError; } + FFI_PLUGIN_EXPORT float getTextureValue(int row, int column) { + return texture2D[row][column]; + } + /// Get the sound length in seconds /// /// [soundHash] the sound hash @@ -937,7 +1004,7 @@ extern "C" std::vector pNames = player.get()->mFilters.getFilterParamNames(filterType); *paramsCount = static_cast(pNames.size()); *names = (char *)malloc(sizeof(char *) * *paramsCount); - printf("C paramsCount: %p **names: %p\n", paramsCount, names); + // printf("C paramsCount: %p **names: %p\n", paramsCount, names); for (int i = 0; i < *paramsCount; i++) { names[i] = strdup(pNames[i].c_str()); diff --git a/src/bindings_capture.cpp b/src/bindings_capture.cpp index 67b6953..93437f9 100644 --- a/src/bindings_capture.cpp +++ b/src/bindings_capture.cpp @@ -11,106 +11,142 @@ #include #ifdef __cplusplus -extern "C" { +extern "C" +{ #endif -Capture capture; -std::unique_ptr analyzerCapture = std::make_unique(256); + Capture capture; + std::unique_ptr analyzerCapture = std::make_unique(256); -FFI_PLUGIN_EXPORT void listCaptureDevices(struct CaptureDevice **devices, int *n_devices) -{ - std::vector d = capture.listCaptureDevices(); - int numDevices = 0; - for (int i=0; i<(int)d.size(); i++) + FFI_PLUGIN_EXPORT void listCaptureDevices( + char **devicesName, + int **isDefault, + int *n_devices) { - bool hasSpecialChar = false; - /// check if the device name has some strange chars (happens on linux) - for (int n=0; n<5; n++) if (d[i].name[n] < 0x20) hasSpecialChar = true; - if (strlen(d[i].name) <= 5 || hasSpecialChar) break; - - devices[i] = (CaptureDevice*)malloc(sizeof(CaptureDevice)); - devices[i]->name = strdup(d[i].name); - devices[i]->isDefault = d[i].isDefault; + std::vector d = capture.listCaptureDevices(); + + int numDevices = 0; + for (int i = 0; i < (int)d.size(); i++) + { + bool hasSpecialChar = false; + /// check if the device name has some strange chars (happens on linux) + for (int n = 0; n < 5; n++) + if (d[i].name[n] < 0x20) + hasSpecialChar = true; + if (strlen(d[i].name) <= 5 || hasSpecialChar) + continue; + + devicesName[i] = strdup(d[i].name); + isDefault[i] = (int *)malloc(sizeof(int *)); + *isDefault[i] = d[i].isDefault; + + numDevices++; + } + *n_devices = numDevices; + } - numDevices++; + FFI_PLUGIN_EXPORT void freeListCaptureDevices( + char **devicesName, + int **isDefault, + int n_devices) + { + for (int i = 0; i < n_devices; i++) + { + free(devicesName[i]); + free(isDefault[i]); + } } - *n_devices = numDevices; -} -FFI_PLUGIN_EXPORT void freeListCaptureDevices(struct CaptureDevice **devices, int n_devices) -{ - for (int i=0; iname); - free(devices[i]); + CaptureErrors res = capture.init(deviceID); + return res; } -} -FFI_PLUGIN_EXPORT enum CaptureErrors initCapture(int deviceID) -{ - CaptureErrors res = capture.init(deviceID); - return res; -} + FFI_PLUGIN_EXPORT void disposeCapture() + { + capture.dispose(); + } -FFI_PLUGIN_EXPORT void disposeCapture() -{ - capture.dispose(); -} + FFI_PLUGIN_EXPORT int isCaptureInited() + { + return capture.isInited() ? 1 : 0; + } -FFI_PLUGIN_EXPORT int isCaptureInited() -{ - return capture.isInited() ? 1 : 0; -} + FFI_PLUGIN_EXPORT int isCaptureStarted() + { + return capture.isStarted() ? 1 : 0; + } -FFI_PLUGIN_EXPORT int isCaptureStarted() -{ - return capture.isStarted() ? 1 : 0; -} + FFI_PLUGIN_EXPORT enum CaptureErrors startCapture() + { + return capture.startCapture(); + } -FFI_PLUGIN_EXPORT enum CaptureErrors startCapture() -{ - return capture.startCapture(); -} + FFI_PLUGIN_EXPORT enum CaptureErrors stopCapture() + { + return capture.stopCapture(); + } -FFI_PLUGIN_EXPORT enum CaptureErrors stopCapture() -{ - return capture.stopCapture(); -} + /// Return a 256 float array containing FFT data. + FFI_PLUGIN_EXPORT void getCaptureFft(float **fft) + { + if (!capture.isInited()) + return; + float *wave = capture.getWave(); + *fft = analyzerCapture.get()->calcFFT(wave); + } + /// Return a 256 float array containing wave data. + FFI_PLUGIN_EXPORT void getCaptureWave(float **wave) + { + if (!capture.isInited()) + return; + *wave = capture.getWave(); + } -FFI_PLUGIN_EXPORT void getCaptureTexture(float* samples) -{ - if (analyzerCapture.get() == nullptr || !capture.isInited()) { - memset(samples,0, sizeof(float) * 512); - return; + FFI_PLUGIN_EXPORT void getCaptureTexture(float *samples) + { + if (analyzerCapture.get() == nullptr || !capture.isInited()) + { + memset(samples, 0, sizeof(float) * 512); + return; + } + float *wave = capture.getWave(); + float *fft = analyzerCapture.get()->calcFFT(wave); + + memcpy(samples, fft, sizeof(float) * 256); + memcpy(samples + 256, wave, sizeof(float) * 256); } - float *wave = capture.getWave(); - float *fft = analyzerCapture.get()->calcFFT(wave); - memcpy(samples, fft, sizeof(float) * 256); - memcpy(samples + 256, wave, sizeof(float) * 256); -} + float capturedTexture2D[256][512]; + FFI_PLUGIN_EXPORT enum CaptureErrors getCaptureAudioTexture2D(float **samples) + { + if (analyzerCapture.get() == nullptr || !capture.isInited()) + { + *samples = *capturedTexture2D; + memset(*samples, 0, sizeof(float) * 512 * 256); + return capture_not_inited; + } + /// shift up 1 row + memmove(*capturedTexture2D + 512, capturedTexture2D, sizeof(float) * 512 * 255); + /// store the new 1st row + getCaptureTexture(capturedTexture2D[0]); + *samples = *capturedTexture2D; + return capture_noError; + } -float capturedTexture2D[256][512]; -FFI_PLUGIN_EXPORT enum CaptureErrors getCaptureAudioTexture2D(float** samples) -{ - if (analyzerCapture.get() == nullptr || !capture.isInited()) { - memset(samples,0, sizeof(float) * 512 * 256); - return capture_not_inited; + FFI_PLUGIN_EXPORT float getCaptureTextureValue(int row, int column) { + return capturedTexture2D[row][column]; } - /// shift up 1 row - memmove(*capturedTexture2D+512, capturedTexture2D, sizeof(float) * 512 * 255); - /// store the new 1st row - getCaptureTexture(capturedTexture2D[0]); - *samples = *capturedTexture2D; - return capture_noError; -} -FFI_PLUGIN_EXPORT enum CaptureErrors setCaptureFftSmoothing(float smooth) -{ - if (!capture.isInited()) return capture_not_inited; - analyzerCapture.get()->setSmoothing(smooth); - return capture_noError; -} + FFI_PLUGIN_EXPORT enum CaptureErrors setCaptureFftSmoothing(float smooth) + { + if (!capture.isInited()) + return capture_not_inited; + analyzerCapture.get()->setSmoothing(smooth); + return capture_noError; + } #ifdef __cplusplus } diff --git a/src/enums.h b/src/enums.h index 1d2832f..96beb7c 100644 --- a/src/enums.h +++ b/src/enums.h @@ -51,11 +51,11 @@ typedef enum CaptureErrors { /// No error capture_noError, - /// + /// The capture device has failed to initialize. capture_init_failed, - /// + /// The capture device has not yet been initialized. capture_not_inited, - /// + /// Failed to start the device. failed_to_start_device, } CaptureErrors_t; diff --git a/src/ffi_gen_tmp.h b/src/ffi_gen_tmp.h index ae0a34f..fbda9cc 100644 --- a/src/ffi_gen_tmp.h +++ b/src/ffi_gen_tmp.h @@ -23,16 +23,9 @@ struct CaptureDevice //--------------------- copy here the new functions to generate - -typedef void (*dartVoiceEndedCallback_t)(unsigned int *); -typedef void (*dartFileLoadedCallback_t)(enum PlayerErrors *, char *completeFileName, unsigned int *); -typedef void (*dartStateChangedCallback_t)(enum PlayerStateEvents *); - -/// Set a Dart functions to call when an event occurs. -/// -FFI_PLUGIN_EXPORT void setDartEventCallback( - dartVoiceEndedCallback_t voice_ended_callback, - dartFileLoadedCallback_t file_loaded_callback, - dartStateChangedCallback_t state_changed_callback); - -FFI_PLUGIN_EXPORT void nativeFree(void *pointer); \ No newline at end of file +FFI_PLUGIN_EXPORT enum PlayerErrors loadMem( + char *uniqueName, + unsigned char *buffer, + int length, + int loadIntoMem, + unsigned int *hash); diff --git a/src/filters/filters.cpp b/src/filters/filters.cpp index cf915a4..b69a564 100644 --- a/src/filters/filters.cpp +++ b/src/filters/filters.cpp @@ -61,6 +61,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case LofiFilter: { SoLoud::LofiFilter f; @@ -70,6 +71,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case FlangerFilter: { SoLoud::FlangerFilter f; @@ -79,6 +81,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case BassboostFilter: { SoLoud::BassboostFilter f; @@ -88,6 +91,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case WaveShaperFilter: { SoLoud::WaveShaperFilter f; @@ -97,6 +101,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case RobotizeFilter: { SoLoud::RobotizeFilter f; @@ -106,6 +111,7 @@ std::vector Filters::getFilterParamNames(FilterType filterType) ret.push_back(f.getParamName(i)); } } + break; case FreeverbFilter: { SoLoud::FreeverbFilter f; diff --git a/src/player.cpp b/src/player.cpp index ff9d331..c46fee8 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -170,6 +170,7 @@ PlayerErrors Player::loadMem( const std::string &uniqueName, unsigned char *mem, int length, + bool loadIntoMem, unsigned int &hash) { if (!mInited) @@ -195,9 +196,18 @@ PlayerErrors Player::loadMem( hash = sounds.back().get()->soundHash = newHash; SoLoud::result result; - sounds.back().get()->sound = std::make_shared(); - sounds.back().get()->soundType = TYPE_WAV; - result = static_cast(sounds.back().get()->sound.get())->loadMem(mem, length, true, true); + if (loadIntoMem) + { + sounds.back().get()->sound = std::make_shared(); + sounds.back().get()->soundType = TYPE_WAV; + result = static_cast(sounds.back().get()->sound.get())->loadMem(mem, length, false, true); + } + else + { + sounds.back().get()->sound = std::make_shared(); + sounds.back().get()->soundType = TYPE_WAVSTREAM; + result = static_cast(sounds.back().get()->sound.get())->loadMem(mem, length, false, true); + } if (result != SoLoud::SO_NO_ERROR) { @@ -329,22 +339,16 @@ unsigned int Player::play( bool looping, double loopingStartAt) { - // printf("*** PLAYER:PLAY() sounds length: %d looking for hash: %u\n", sounds.size(), soundHash); - - // for (int i = 0; i < sounds.size(); i++) - // printf("*** PLAYER:PLAY()1 sounds hash: %u\n", sounds[i].get()->soundHash); - auto const &s = std::find_if( sounds.begin(), sounds.end(), [&](std::shared_ptr const &f) - { - // printf("*** PLAYER:PLAY() sound1 hash: %u\n", f->soundHash); - return f->soundHash == soundHash; }); + { return f->soundHash == soundHash; }); if (s == sounds.end()) return 0; ActiveSound *sound = s->get(); + SoLoud::handle newHandle = soloud.play( *sound->sound.get(), volume, pan, paused, 0); if (newHandle != 0) @@ -374,14 +378,16 @@ void Player::removeHandle(unsigned int handle) // { return f == handle; })); bool e = true; for (int i = 0; i < sounds.size(); ++i) - for (int n = 0; n< sounds[i]->handle.size(); ++n) + for (int n = 0; n < sounds[i]->handle.size(); ++n) { - if (sounds[i]->handle[n] == handle) { - sounds[i]->handle.erase(sounds[i]->handle.begin()+n); + if (sounds[i]->handle[n] == handle) + { + sounds[i]->handle.erase(sounds[i]->handle.begin() + n); e = false; break; } - if (e) break; + if (e) + break; } } @@ -545,7 +551,6 @@ void Player::setPanAbsolute(SoLoud::handle handle, float panLeft, float panRight soloud.setPanAbsolute(handle, panLeft, panRight); } - bool Player::isValidVoiceHandle(SoLoud::handle handle) { return soloud.isValidVoiceHandle(handle); diff --git a/src/player.h b/src/player.h index 5cec0c9..0dcf786 100644 --- a/src/player.h +++ b/src/player.h @@ -98,6 +98,7 @@ class Player const std::string &uniqueName, unsigned char *mem, int length, + bool loadIntoMem, unsigned int &hash); /// @brief Load a new sound which will be generated by the given params. diff --git a/wasm.sh b/wasm.sh new file mode 100644 index 0000000..3a2dcaa --- /dev/null +++ b/wasm.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail + +cd web +sh ./compile_wasm.sh diff --git a/web.sh b/web.sh new file mode 100644 index 0000000..784d1b3 --- /dev/null +++ b/web.sh @@ -0,0 +1,5 @@ +#!/bin/bash +set -euo pipefail + +cd web +sh ./compile_worker.sh diff --git a/web/CMakeLists.txt b/web/CMakeLists.txt deleted file mode 100644 index 2ef04c0..0000000 --- a/web/CMakeLists.txt +++ /dev/null @@ -1,55 +0,0 @@ -cmake_minimum_required(VERSION 3.10) -set(PROJECT_NAME "flutter_soloud") -project(${PROJECT_NAME} LANGUAGES CXX) -set(CMAKE_CXX_STANDARD 14) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -set(PLUGIN_NAME "${PROJECT_NAME}_plugin") - -if (EMSCRIPTEN) - set(CMAKE_AR "emcc") - set(CMAKE_STATIC_LIBRARY_SUFFIX ".js") - set(CMAKE_C_CREATE_STATIC_LIBRARY " -o ") - set(CMAKE_CXX_CREATE_STATIC_LIBRARY " -o ") -endif() - -set(CMAKE_C_FLAGS "-s STANDALONE_WASM" ) - - -## Add SoLoud custom cmake files -message("**************** SOLOUD CONFIGURE.CMAKE") -include (Configure.cmake) -message("**************** SOLOUD SRC.CMAKE 1") -include_directories(../src/soloud/include ) -include_directories(../src/soloud/src) -include (src.cmake) -message("**************** SOLOUD SRC.CMAKE 2 ${TARGET_NAME}") - - -list(APPEND PLUGIN_SOURCES - "../src/common.cpp" - "../src/bindings.cpp" - "../src/player.cpp" - "../src/analyzer.cpp" - "../src/bindings_capture.cpp" - "../src/capture.cpp" - "../src/synth/basic_wave.cpp" - "../src/filters/filters.cpp" - - # add SoLoud sources. These definitions are in src.cmake - ${TARGET_SOURCES} -) -add_library(${PLUGIN_NAME} STATIC - ${PLUGIN_SOURCES} -) - - -target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) -target_include_directories(${PLUGIN_NAME} INTERFACE "${CMAKE_CURRENT_SOURCE_DIR}/src") -target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) - - -target_compile_options(${PLUGIN_NAME} PRIVATE -Wall -Wno-error -O3 -DNDEBUG -s MAIN_MODULE=1) - -target_compile_definitions (${PLUGIN_NAME} PRIVATE __WASM__) - diff --git a/web/Configure.cmake b/web/Configure.cmake deleted file mode 100644 index 68252e1..0000000 --- a/web/Configure.cmake +++ /dev/null @@ -1,48 +0,0 @@ -# copied from souloud/contrib to set these parameters for linux - -include (${CMAKE_CURRENT_SOURCE_DIR}/../src/soloud/contrib/cmake/OptionDependentOnPackage.cmake) -include (${CMAKE_CURRENT_SOURCE_DIR}/../src/soloud/contrib/cmake/PrintOptionStatus.cmake) - -option (SOLOUD_DYNAMIC "Set to ON to build dynamic SoLoud" OFF) -print_option_status (SOLOUD_DYNAMIC "Build dynamic library") - -option (SOLOUD_STATIC "Set to ON to build static SoLoud" ON) -print_option_status (SOLOUD_STATIC "Build static library") - -option (SOLOUD_C_API "Set to ON to include the C API" OFF) -print_option_status (SOLOUD_C_API "Build C API") - - -# TODO: -option (SOLOUD_BACKEND_MINIAUDIO "Set to ON to include MiniAudio" ON) -print_option_status (SOLOUD_WITH_MINIAUDIO "Build MiniAudio") - -option (SOLOUD_BUILD_DEMOS "Set to ON for building demos" OFF) -print_option_status (SOLOUD_BUILD_DEMOS "Build demos") - -option (SOLOUD_BACKEND_NULL "Set to ON for building NULL backend" ON) -print_option_status (SOLOUD_BACKEND_NULL "NULL backend") - -option (SOLOUD_BACKEND_SDL2 "Set to ON for building SDL2 backend" OFF) -print_option_status (SOLOUD_BACKEND_SDL2 "SDL2 backend") - -option (SOLOUD_BACKEND_ALSA "Set to ON for building ALSA backend" OFF) -print_option_status (SOLOUD_BACKEND_ALSA "ALSA backend") - -option (SOLOUD_BACKEND_COREAUDIO "Set to ON for building CoreAudio backend" OFF) -print_option_status (SOLOUD_BACKEND_COREAUDIO "CoreAudio backend") - -option (SOLOUD_BACKEND_OPENSLES "Set to ON for building OpenSLES backend" OFF) -print_option_status (SOLOUD_BACKEND_OPENSLES "OpenSLES backend") - -option (SOLOUD_BACKEND_XAUDIO2 "Set to ON for building XAudio2 backend" OFF) -print_option_status (SOLOUD_BACKEND_XAUDIO2 "XAudio2 backend") - -option (SOLOUD_BACKEND_WINMM "Set to ON for building WINMM backend" OFF) -print_option_status (SOLOUD_BACKEND_WINMM "WINMM backend") - -option (SOLOUD_BACKEND_WASAPI "Set to ON for building WASAPI backend" OFF) -print_option_status (SOLOUD_BACKEND_WASAPI "WASAPI backend") - -option (SOLOUD_GENERATE_GLUE "Set to ON for generating the Glue APIs" OFF) -print_option_status (SOLOUD_GENERATE_GLUE "Generate Glue") diff --git a/web/compile b/web/compile deleted file mode 100755 index def2531..0000000 --- a/web/compile +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -rm -r build -mkdir build -cd build -emcmake cmake .. -emmake make -j30 diff --git a/web/compile_wasm.sh b/web/compile_wasm.sh new file mode 100755 index 0000000..2e19361 --- /dev/null +++ b/web/compile_wasm.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +rm -f libflutter_soloud_plugin.* +rm -r build +mkdir build +cd build + + +#https://emscripten.org/docs/tools_reference/emcc.html +#-g3 #keep debug info, including JS whitespace, function names +#-sSTACK_SIZE=1048576 -sALLOW_MEMORY_GROWTH + +# disable the asynchronous startup/loading behaviour +# -s BINARYEN_ASYNC_COMPILATION=0 +# https://github.com/emscripten-core/emscripten/issues/5352#issuecomment-312384604 + +# https://emscripten.org/docs/tools_reference/settings_reference.html + +# -s ASSERTIONS=1 +# -s TOTAL_MEMORY=512MB \ +# -s DEFAULT_TO_CXX \ +# -s STACK_SIZE=1048576 + +## Compiling with "-O2" or "-O3" doesn't work + +em++ \ +-I ../../src -I ../../src/filters -I ../../src/synth -I ../../src/soloud/include \ +-I ../../src/soloud/src -I ../../src/soloud/include \ +../../src/soloud/src/core/*.c* \ +../../src/soloud/src/filter/*.c* \ +../../src/soloud/src/backend/miniaudio/*.c* \ +../../src/soloud/src/audiosource/ay/*.c* \ +../../src/soloud/src/audiosource/speech/*.c* \ +../../src/soloud/src/audiosource/wav/*.c* \ +../../src/common.cpp \ +../../src/bindings.cpp \ +../../src/player.cpp \ +../../src/analyzer.cpp \ +../../src/bindings_capture.cpp \ +../../src/capture.cpp \ +../../src/synth/basic_wave.cpp \ +../../src/filters/filters.cpp \ +-O3 -D WITH_MINIAUDIO \ +-I ~/.emscripten_cache/sysroot/include \ +-s "EXPORTED_RUNTIME_METHODS=['ccall','cwrap']" \ +-s "EXPORTED_FUNCTIONS=['_free', '_malloc']" \ +-s EXPORT_ALL=1 -s NO_EXIT_RUNTIME=1 \ +-s SAFE_HEAP=1 \ +-s ALLOW_MEMORY_GROWTH \ +-o ../../web/libflutter_soloud_plugin.js diff --git a/web/compile_worker.bat b/web/compile_worker.bat new file mode 100755 index 0000000..8ed2dea --- /dev/null +++ b/web/compile_worker.bat @@ -0,0 +1,3 @@ + # -O4 +dart compile js -o worker.dart.js ./worker.dart + diff --git a/web/compile_worker.sh b/web/compile_worker.sh new file mode 100755 index 0000000..450968f --- /dev/null +++ b/web/compile_worker.sh @@ -0,0 +1,2 @@ +dart compile js -O3 -o worker.dart.js ./worker.dart + diff --git a/web/libflutter_soloud_plugin.js b/web/libflutter_soloud_plugin.js new file mode 100644 index 0000000..6a76334 --- /dev/null +++ b/web/libflutter_soloud_plugin.js @@ -0,0 +1 @@ +var Module=typeof Module!="undefined"?Module:{};var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string";if(ENVIRONMENT_IS_NODE){}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("fs");var nodePath=require("path");scriptDirectory=__dirname+"/";read_=(filename,binary)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return fs.readFileSync(filename,binary?undefined:"utf8")};readBinary=filename=>{var ret=read_(filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}return ret};readAsync=(filename,onload,onerror,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)onerror(err);else onload(binary?data.buffer:data)})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);if(typeof module!="undefined"){module["exports"]=Module}process.on("uncaughtException",ex=>{if(ex!=="unwind"&&!(ex instanceof ExitStatus)&&!(ex.context instanceof ExitStatus)){throw ex}});quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{read_=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=(url,onload,onerror)=>{if(isFileURI(url)){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=()=>{if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null);return}fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))}).then(onload,onerror)}}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];function getSafeHeapType(bytes,isFloat){switch(bytes){case 1:return"i8";case 2:return"i16";case 4:return isFloat?"float":"i32";case 8:return isFloat?"double":"i64";default:abort(`getSafeHeapType() invalid bytes=${bytes}`)}}function SAFE_HEAP_STORE(dest,value,bytes,isFloat){if(dest<=0)abort(`segmentation fault storing ${bytes} bytes to address ${dest}`);if(dest%bytes!==0)abort(`alignment error storing to address ${dest}, which was expected to be aligned to a multiple of ${bytes}`);if(runtimeInitialized){var brk=_sbrk(0);if(dest+bytes>brk)abort(`segmentation fault, exceeded the top of the available dynamic heap when storing ${bytes} bytes to address ${dest}. DYNAMICTOP=${brk}`);if(brk<_emscripten_stack_get_base())abort(`brk >= _emscripten_stack_get_base() (brk=${brk}, _emscripten_stack_get_base()=${_emscripten_stack_get_base()})`);if(brk>wasmMemory.buffer.byteLength)abort(`brk <= wasmMemory.buffer.byteLength (brk=${brk}, wasmMemory.buffer.byteLength=${wasmMemory.buffer.byteLength})`)}setValue_safe(dest,value,getSafeHeapType(bytes,isFloat));return value}function SAFE_HEAP_STORE_D(dest,value,bytes){return SAFE_HEAP_STORE(dest,value,bytes,true)}function SAFE_HEAP_LOAD(dest,bytes,unsigned,isFloat){if(dest<=0)abort(`segmentation fault loading ${bytes} bytes from address ${dest}`);if(dest%bytes!==0)abort(`alignment error loading from address ${dest}, which was expected to be aligned to a multiple of ${bytes}`);if(runtimeInitialized){var brk=_sbrk(0);if(dest+bytes>brk)abort(`segmentation fault, exceeded the top of the available dynamic heap when loading ${bytes} bytes from address ${dest}. DYNAMICTOP=${brk}`);if(brk<_emscripten_stack_get_base())abort(`brk >= _emscripten_stack_get_base() (brk=${brk}, _emscripten_stack_get_base()=${_emscripten_stack_get_base()})`);if(brk>wasmMemory.buffer.byteLength)abort(`brk <= wasmMemory.buffer.byteLength (brk=${brk}, wasmMemory.buffer.byteLength=${wasmMemory.buffer.byteLength})`)}var type=getSafeHeapType(bytes,isFloat);var ret=getValue_safe(dest,type);if(unsigned)ret=unSign(ret,parseInt(type.substr(1),10));return ret}function SAFE_HEAP_LOAD_D(dest,bytes,unsigned){return SAFE_HEAP_LOAD(dest,bytes,unsigned,true)}function segfault(){abort("segmentation fault")}function alignfault(){abort("alignment fault")}var wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.init.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;EXITSTATUS=1;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function findWasmBinary(){var f="libflutter_soloud_plugin.wasm";if(!isDataURI(f)){return locateFile(f)}return f}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return new Promise((resolve,reject)=>{readAsync(binaryFile,response=>resolve(new Uint8Array(response)),error=>{try{resolve(getBinarySync(binaryFile))}catch(e){reject(e)}})})}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!isFileURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{a:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["q"];updateMemoryViews();addOnInit(wasmExports["r"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);return false}}if(!wasmBinaryFile)wasmBinaryFile=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult);return{}}var tempDouble;var tempI64;var ASM_CONSTS={67376:($0,$1,$2,$3,$4)=>{if(typeof window==="undefined"||(window.AudioContext||window.webkitAudioContext)===undefined){return 0}if(typeof window.miniaudio==="undefined"){window.miniaudio={referenceCount:0};window.miniaudio.device_type={};window.miniaudio.device_type.playback=$0;window.miniaudio.device_type.capture=$1;window.miniaudio.device_type.duplex=$2;window.miniaudio.device_state={};window.miniaudio.device_state.stopped=$3;window.miniaudio.device_state.started=$4;let miniaudio=window.miniaudio;miniaudio.devices=[];miniaudio.track_device=function(device){for(var iDevice=0;iDevice0){if(miniaudio.devices[miniaudio.devices.length-1]==null){miniaudio.devices.pop()}else{break}}};miniaudio.untrack_device=function(device){for(var iDevice=0;iDevice{_ma_device__on_notification_unlocked(device.pDevice)},error=>{console.error("Failed to resume audiocontext",error)})}}miniaudio.unlock_event_types.map(function(event_type){document.removeEventListener(event_type,miniaudio.unlock,true)})};miniaudio.unlock_event_types.map(function(event_type){document.addEventListener(event_type,miniaudio.unlock,true)})}window.miniaudio.referenceCount+=1;return 1},69554:()=>{if(typeof window.miniaudio!=="undefined"){miniaudio.unlock_event_types.map(function(event_type){document.removeEventListener(event_type,miniaudio.unlock,true)});window.miniaudio.referenceCount-=1;if(window.miniaudio.referenceCount===0){delete window.miniaudio}}},69844:()=>navigator.mediaDevices!==undefined&&navigator.mediaDevices.getUserMedia!==undefined,69948:()=>{try{var temp=new(window.AudioContext||window.webkitAudioContext);var sampleRate=temp.sampleRate;temp.close();return sampleRate}catch(e){return 0}},70119:($0,$1,$2,$3,$4,$5)=>{var deviceType=$0;var channels=$1;var sampleRate=$2;var bufferSize=$3;var pIntermediaryBuffer=$4;var pDevice=$5;if(typeof window.miniaudio==="undefined"){return-1}var device={};var audioContextOptions={};if(deviceType==window.miniaudio.device_type.playback&&sampleRate!=0){audioContextOptions.sampleRate=sampleRate}device.webaudio=new(window.AudioContext||window.webkitAudioContext)(audioContextOptions);device.webaudio.suspend();device.state=window.miniaudio.device_state.stopped;var channelCountIn=0;var channelCountOut=channels;if(deviceType!=window.miniaudio.device_type.playback){channelCountIn=channels}device.scriptNode=device.webaudio.createScriptProcessor(bufferSize,channelCountIn,channelCountOut);device.scriptNode.onaudioprocess=function(e){if(device.intermediaryBufferView==null||device.intermediaryBufferView.length==0){device.intermediaryBufferView=new Float32Array(HEAPF32.buffer,pIntermediaryBuffer,bufferSize*channels)}if(deviceType==window.miniaudio.device_type.capture||deviceType==window.miniaudio.device_type.duplex){for(var iChannel=0;iChannelwindow.miniaudio.get_device_by_index($0).webaudio.sampleRate,73069:$0=>{var device=window.miniaudio.get_device_by_index($0);if(device.scriptNode!==undefined){device.scriptNode.onaudioprocess=function(e){};device.scriptNode.disconnect();device.scriptNode=undefined}if(device.streamNode!==undefined){device.streamNode.disconnect();device.streamNode=undefined}device.webaudio.close();device.webaudio=undefined;device.pDevice=undefined},73469:$0=>{window.miniaudio.untrack_device_by_index($0)},73519:$0=>{var device=window.miniaudio.get_device_by_index($0);device.webaudio.resume();device.state=window.miniaudio.device_state.started},73658:$0=>{var device=window.miniaudio.get_device_by_index($0);device.webaudio.suspend();device.state=window.miniaudio.device_state.stopped},73798:()=>{if(!Module.wasmWorker){var workerUri="assets/packages/flutter_soloud/web/worker.dart.js";console.log("EM_ASM creating web worker!");Module.wasmWorker=new Worker(workerUri)}else{console.log("EM_ASM web worker already created!")}},74046:($0,$1)=>{if(Module.wasmWorker){console.log("EM_ASM posting message "+UTF8ToString($0)+" with value "+$1);Module.wasmWorker.postMessage(JSON.stringify({message:UTF8ToString($0),value:$1}))}else{console.error("Worker not found.")}}};function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}Module["ExitStatus"]=ExitStatus;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};Module["callRuntimeCallbacks"]=callRuntimeCallbacks;function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return SAFE_HEAP_LOAD(ptr,1,0);case"i8":return SAFE_HEAP_LOAD(ptr,1,0);case"i16":return SAFE_HEAP_LOAD((ptr>>1)*2,2,0);case"i32":return SAFE_HEAP_LOAD((ptr>>2)*4,4,0);case"i64":abort("to do getValue(i64) use WASM_BIGINT");case"float":return SAFE_HEAP_LOAD_D((ptr>>2)*4,4,0);case"double":return SAFE_HEAP_LOAD_D((ptr>>3)*8,8,0);case"*":return SAFE_HEAP_LOAD((ptr>>2)*4,4,1);default:abort(`invalid type for getValue: ${type}`)}}Module["getValue"]=getValue;function getValue_safe(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":abort("to do getValue(i64) use WASM_BIGINT");case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}Module["getValue_safe"]=getValue_safe;var noExitRuntime=Module["noExitRuntime"]||true;Module["noExitRuntime"]=noExitRuntime;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":SAFE_HEAP_STORE(ptr,value,1);break;case"i8":SAFE_HEAP_STORE(ptr,value,1);break;case"i16":SAFE_HEAP_STORE((ptr>>1)*2,value,2);break;case"i32":SAFE_HEAP_STORE((ptr>>2)*4,value,4);break;case"i64":abort("to do setValue(i64) use WASM_BIGINT");case"float":SAFE_HEAP_STORE_D((ptr>>2)*4,value,4);break;case"double":SAFE_HEAP_STORE_D((ptr>>3)*8,value,8);break;case"*":SAFE_HEAP_STORE((ptr>>2)*4,value,4);break;default:abort(`invalid type for setValue: ${type}`)}}Module["setValue"]=setValue;function setValue_safe(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":abort("to do setValue(i64) use WASM_BIGINT");case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}Module["setValue_safe"]=setValue_safe;var stackRestore=val=>__emscripten_stack_restore(val);Module["stackRestore"]=stackRestore;var stackSave=()=>_emscripten_stack_get_current();Module["stackSave"]=stackSave;var unSign=(value,bits)=>{if(value>=0){return value}return bits<=32?2*Math.abs(1<{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};Module["UTF8ArrayToString"]=UTF8ArrayToString;var UTF8ToString=(ptr,maxBytesToRead)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";Module["UTF8ToString"]=UTF8ToString;var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};Module["___assert_fail"]=___assert_fail;class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){SAFE_HEAP_STORE((this.ptr+4>>2)*4,type,4)}get_type(){return SAFE_HEAP_LOAD((this.ptr+4>>2)*4,4,1)}set_destructor(destructor){SAFE_HEAP_STORE((this.ptr+8>>2)*4,destructor,4)}get_destructor(){return SAFE_HEAP_LOAD((this.ptr+8>>2)*4,4,1)}set_caught(caught){caught=caught?1:0;SAFE_HEAP_STORE(this.ptr+12,caught,1)}get_caught(){return SAFE_HEAP_LOAD(this.ptr+12,1,0)!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;SAFE_HEAP_STORE(this.ptr+13,rethrown,1)}get_rethrown(){return SAFE_HEAP_LOAD(this.ptr+13,1,0)!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){SAFE_HEAP_STORE((this.ptr+16>>2)*4,adjustedPtr,4)}get_adjusted_ptr(){return SAFE_HEAP_LOAD((this.ptr+16>>2)*4,4,1)}get_exception_ptr(){var isPointer=___cxa_is_pointer_type(this.get_type());if(isPointer){return SAFE_HEAP_LOAD((this.excPtr>>2)*4,4,1)}var adjusted=this.get_adjusted_ptr();if(adjusted!==0)return adjusted;return this.excPtr}}Module["ExceptionInfo"]=ExceptionInfo;var exceptionLast=0;Module["exceptionLast"]=exceptionLast;var uncaughtExceptionCount=0;Module["uncaughtExceptionCount"]=uncaughtExceptionCount;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};Module["___cxa_throw"]=___cxa_throw;function syscallGetVarargI(){var ret=SAFE_HEAP_LOAD((+SYSCALLS.varargs>>2)*4,4,0);SYSCALLS.varargs+=4;return ret}Module["syscallGetVarargI"]=syscallGetVarargI;var syscallGetVarargP=syscallGetVarargI;Module["syscallGetVarargP"]=syscallGetVarargP;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};Module["PATH"]=PATH;var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("initRandomDevice")};Module["initRandomFill"]=initRandomFill;var randomFill=view=>(randomFill=initRandomFill())(view);Module["randomFill"]=randomFill;var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};Module["lengthBytesUTF8"]=lengthBytesUTF8;var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};Module["stringToUTF8Array"]=stringToUTF8Array;function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}Module["intArrayFromString"]=intArrayFromString;var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};Module["FS_stdin_getChar"]=FS_stdin_getChar;var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops:ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output,0));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output,0));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output,0));tty.output=[]}}}};Module["TTY"]=TTY;var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size);return address};Module["zeroMemory"]=zeroMemory;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;Module["alignMemory"]=alignMemory;var mmapAlloc=size=>{abort()};Module["mmapAlloc"]=mmapAlloc;var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url,arrayBuffer=>{onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},event=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};Module["asyncLoad"]=asyncLoad;var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};Module["FS_createDataFile"]=FS_createDataFile;var preloadPlugins=Module["preloadPlugins"]||[];Module["preloadPlugins"]=preloadPlugins;var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};Module["FS_handledByPreloadPlugin"]=FS_handledByPreloadPlugin;var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};Module["FS_createPreloadedFile"]=FS_createPreloadedFile;var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};Module["FS_modeStringToFlags"]=FS_modeStringToFlags;var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};Module["FS_getMode"]=FS_getMode;var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class{constructor(errno){this.name="ErrnoError";this.errno=errno}},genericErrors:{},filesystems:null,syncFSRequests:0,FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type:type,opts:opts,mountpoint:mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;iFS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(){if(Module["stdin"]){FS.createDevice("/dev","stdin",Module["stdin"])}else{FS.symlink("/dev/tty","/dev/stdin")}if(Module["stdout"]){FS.createDevice("/dev","stdout",null,Module["stdout"])}else{FS.symlink("/dev/tty","/dev/stdout")}if(Module["stderr"]){FS.createDevice("/dev","stderr",null,Module["stderr"])}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS:MEMFS}},init(input,output,error){FS.init.initialized=true;Module["stdin"]=input||Module["stdin"];Module["stdout"]=output||Module["stdout"];Module["stderr"]=error||Module["stderr"];FS.createStandardStreams()},quit(){FS.init.initialized=false;for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url:url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr:ptr,allocated:true}};node.stream_ops=stream_ops;return node}};Module["FS"]=FS;var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);SAFE_HEAP_STORE((buf>>2)*4,stat.dev,4);SAFE_HEAP_STORE((buf+4>>2)*4,stat.mode,4);SAFE_HEAP_STORE((buf+8>>2)*4,stat.nlink,4);SAFE_HEAP_STORE((buf+12>>2)*4,stat.uid,4);SAFE_HEAP_STORE((buf+16>>2)*4,stat.gid,4);SAFE_HEAP_STORE((buf+20>>2)*4,stat.rdev,4);tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((buf+24>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((buf+28>>2)*4,tempI64[1],4);SAFE_HEAP_STORE((buf+32>>2)*4,4096,4);SAFE_HEAP_STORE((buf+36>>2)*4,stat.blocks,4);var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((buf+40>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((buf+44>>2)*4,tempI64[1],4);SAFE_HEAP_STORE((buf+48>>2)*4,atime%1e3*1e3,4);tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((buf+56>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((buf+60>>2)*4,tempI64[1],4);SAFE_HEAP_STORE((buf+64>>2)*4,mtime%1e3*1e3,4);tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((buf+72>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((buf+76>>2)*4,tempI64[1],4);SAFE_HEAP_STORE((buf+80>>2)*4,ctime%1e3*1e3,4);tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((buf+88>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((buf+92>>2)*4,tempI64[1],4);return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};Module["SYSCALLS"]=SYSCALLS;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;SAFE_HEAP_STORE((arg+offset>>1)*2,2,2);return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}Module["___syscall_fcntl64"]=___syscall_fcntl64;function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();SAFE_HEAP_STORE((argp>>2)*4,termios.c_iflag||0,4);SAFE_HEAP_STORE((argp+4>>2)*4,termios.c_oflag||0,4);SAFE_HEAP_STORE((argp+8>>2)*4,termios.c_cflag||0,4);SAFE_HEAP_STORE((argp+12>>2)*4,termios.c_lflag||0,4);for(var i=0;i<32;i++){SAFE_HEAP_STORE(argp+i+17,termios.c_cc[i]||0,1)}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=SAFE_HEAP_LOAD((argp>>2)*4,4,0);var c_oflag=SAFE_HEAP_LOAD((argp+4>>2)*4,4,0);var c_cflag=SAFE_HEAP_LOAD((argp+8>>2)*4,4,0);var c_lflag=SAFE_HEAP_LOAD((argp+12>>2)*4,4,0);var c_cc=[];for(var i=0;i<32;i++){c_cc.push(SAFE_HEAP_LOAD(argp+i+17,1,0))}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag:c_iflag,c_oflag:c_oflag,c_cflag:c_cflag,c_lflag:c_lflag,c_cc:c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();SAFE_HEAP_STORE((argp>>2)*4,0,4);return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();SAFE_HEAP_STORE((argp>>1)*2,winsize[0],2);SAFE_HEAP_STORE((argp+2>>1)*2,winsize[1],2)}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}Module["___syscall_ioctl"]=___syscall_ioctl;function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}Module["___syscall_openat"]=___syscall_openat;var __abort_js=()=>{abort("")};Module["__abort_js"]=__abort_js;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);Module["__emscripten_memcpy_js"]=__emscripten_memcpy_js;var readEmAsmArgsArray=[];Module["readEmAsmArgsArray"]=readEmAsmArgsArray;var readEmAsmArgs=(sigPtr,buf)=>{readEmAsmArgsArray.length=0;var ch;while(ch=SAFE_HEAP_LOAD(sigPtr++,1,1)){var wide=ch!=105;wide&=ch!=112;buf+=wide&&buf%8?4:0;readEmAsmArgsArray.push(ch==112?SAFE_HEAP_LOAD((buf>>2)*4,4,1):ch==105?SAFE_HEAP_LOAD((buf>>2)*4,4,0):SAFE_HEAP_LOAD_D((buf>>3)*8,8,0));buf+=wide?8:4}return readEmAsmArgsArray};Module["readEmAsmArgs"]=readEmAsmArgs;var runEmAsmFunction=(code,sigPtr,argbuf)=>{var args=readEmAsmArgs(sigPtr,argbuf);return ASM_CONSTS[code](...args)};Module["runEmAsmFunction"]=runEmAsmFunction;var _emscripten_asm_const_int=(code,sigPtr,argbuf)=>runEmAsmFunction(code,sigPtr,argbuf);Module["_emscripten_asm_const_int"]=_emscripten_asm_const_int;var getHeapMax=()=>2147483648;Module["getHeapMax"]=getHeapMax;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};Module["growMemory"]=growMemory;var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}var alignUp=(x,multiple)=>x+(multiple-x%multiple)%multiple;for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignUp(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};Module["_emscripten_resize_heap"]=_emscripten_resize_heap;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}Module["_fd_close"]=_fd_close;var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2)*4,4,1);var len=SAFE_HEAP_LOAD((iov+4>>2)*4,4,1);iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2)*4,num,4);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}Module["_fd_read"]=_fd_read;var convertI32PairToI53Checked=(lo,hi)=>hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN;Module["convertI32PairToI53Checked"]=convertI32PairToI53Checked;function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],SAFE_HEAP_STORE((newOffset>>2)*4,tempI64[0],4),SAFE_HEAP_STORE((newOffset+4>>2)*4,tempI64[1],4);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}Module["_fd_seek"]=_fd_seek;var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2)*4,4,1);var len=SAFE_HEAP_LOAD((iov+4>>2)*4,4,1);iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(typeof offset!="undefined"){offset+=curr}}return ret};Module["doWritev"]=doWritev;function _fd_write(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doWritev(stream,iov,iovcnt);SAFE_HEAP_STORE((pnum>>2)*4,num,4);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}Module["_fd_write"]=_fd_write;var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};Module["_getentropy"]=_getentropy;var getCFunc=ident=>{var func=Module["_"+ident];return func};Module["getCFunc"]=getCFunc;var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};Module["writeArrayToMemory"]=writeArrayToMemory;var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);Module["stringToUTF8"]=stringToUTF8;var stackAlloc=sz=>__emscripten_stack_alloc(sz);Module["stackAlloc"]=stackAlloc;var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};Module["stringToUTF8OnStack"]=stringToUTF8OnStack;var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};Module["cwrap"]=cwrap;FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();var wasmImports={a:___assert_fail,h:___cxa_throw,g:___syscall_fcntl64,j:___syscall_ioctl,k:___syscall_openat,o:__abort_js,l:__emscripten_memcpy_js,c:alignfault,d:_emscripten_asm_const_int,p:_emscripten_resize_heap,e:_fd_close,i:_fd_read,m:_fd_seek,f:_fd_write,n:_getentropy,b:segfault};var wasmExports=createWasm();var ___wasm_call_ctors=()=>(___wasm_call_ctors=wasmExports["r"])();var _malloc=Module["_malloc"]=a0=>(_malloc=Module["_malloc"]=wasmExports["t"])(a0);var _free=Module["_free"]=a0=>(_free=Module["_free"]=wasmExports["u"])(a0);var _ma_device__on_notification_unlocked=Module["_ma_device__on_notification_unlocked"]=a0=>(_ma_device__on_notification_unlocked=Module["_ma_device__on_notification_unlocked"]=wasmExports["v"])(a0);var _ma_malloc_emscripten=Module["_ma_malloc_emscripten"]=(a0,a1)=>(_ma_malloc_emscripten=Module["_ma_malloc_emscripten"]=wasmExports["w"])(a0,a1);var _ma_free_emscripten=Module["_ma_free_emscripten"]=(a0,a1)=>(_ma_free_emscripten=Module["_ma_free_emscripten"]=wasmExports["x"])(a0,a1);var _ma_device_process_pcm_frames_capture__webaudio=Module["_ma_device_process_pcm_frames_capture__webaudio"]=(a0,a1,a2)=>(_ma_device_process_pcm_frames_capture__webaudio=Module["_ma_device_process_pcm_frames_capture__webaudio"]=wasmExports["y"])(a0,a1,a2);var _ma_device_process_pcm_frames_playback__webaudio=Module["_ma_device_process_pcm_frames_playback__webaudio"]=(a0,a1,a2)=>(_ma_device_process_pcm_frames_playback__webaudio=Module["_ma_device_process_pcm_frames_playback__webaudio"]=wasmExports["z"])(a0,a1,a2);var _createWorkerInWasm=Module["_createWorkerInWasm"]=()=>(_createWorkerInWasm=Module["_createWorkerInWasm"]=wasmExports["A"])();var _sendToWorker=Module["_sendToWorker"]=(a0,a1)=>(_sendToWorker=Module["_sendToWorker"]=wasmExports["B"])(a0,a1);var _nativeFree=Module["_nativeFree"]=a0=>(_nativeFree=Module["_nativeFree"]=wasmExports["C"])(a0);var _voiceEndedCallback=Module["_voiceEndedCallback"]=a0=>(_voiceEndedCallback=Module["_voiceEndedCallback"]=wasmExports["D"])(a0);var _setDartEventCallback=Module["_setDartEventCallback"]=(a0,a1,a2)=>(_setDartEventCallback=Module["_setDartEventCallback"]=wasmExports["E"])(a0,a1,a2);var _initEngine=Module["_initEngine"]=()=>(_initEngine=Module["_initEngine"]=wasmExports["F"])();var _dispose=Module["_dispose"]=()=>(_dispose=Module["_dispose"]=wasmExports["G"])();var _isInited=Module["_isInited"]=()=>(_isInited=Module["_isInited"]=wasmExports["H"])();var _loadFile=Module["_loadFile"]=(a0,a1)=>(_loadFile=Module["_loadFile"]=wasmExports["I"])(a0,a1);var _loadMem=Module["_loadMem"]=(a0,a1,a2,a3,a4)=>(_loadMem=Module["_loadMem"]=wasmExports["J"])(a0,a1,a2,a3,a4);var _loadWaveform=Module["_loadWaveform"]=(a0,a1,a2,a3,a4)=>(_loadWaveform=Module["_loadWaveform"]=wasmExports["K"])(a0,a1,a2,a3,a4);var _setWaveformScale=Module["_setWaveformScale"]=(a0,a1)=>(_setWaveformScale=Module["_setWaveformScale"]=wasmExports["L"])(a0,a1);var _setWaveformDetune=Module["_setWaveformDetune"]=(a0,a1)=>(_setWaveformDetune=Module["_setWaveformDetune"]=wasmExports["M"])(a0,a1);var _setWaveformFreq=Module["_setWaveformFreq"]=(a0,a1)=>(_setWaveformFreq=Module["_setWaveformFreq"]=wasmExports["N"])(a0,a1);var _setSuperWave=Module["_setSuperWave"]=(a0,a1)=>(_setSuperWave=Module["_setSuperWave"]=wasmExports["O"])(a0,a1);var _setWaveform=Module["_setWaveform"]=(a0,a1)=>(_setWaveform=Module["_setWaveform"]=wasmExports["P"])(a0,a1);var _speechText=Module["_speechText"]=(a0,a1)=>(_speechText=Module["_speechText"]=wasmExports["Q"])(a0,a1);var _pauseSwitch=Module["_pauseSwitch"]=a0=>(_pauseSwitch=Module["_pauseSwitch"]=wasmExports["R"])(a0);var _setPause=Module["_setPause"]=(a0,a1)=>(_setPause=Module["_setPause"]=wasmExports["S"])(a0,a1);var _getPause=Module["_getPause"]=a0=>(_getPause=Module["_getPause"]=wasmExports["T"])(a0);var _setRelativePlaySpeed=Module["_setRelativePlaySpeed"]=(a0,a1)=>(_setRelativePlaySpeed=Module["_setRelativePlaySpeed"]=wasmExports["U"])(a0,a1);var _getRelativePlaySpeed=Module["_getRelativePlaySpeed"]=a0=>(_getRelativePlaySpeed=Module["_getRelativePlaySpeed"]=wasmExports["V"])(a0);var _play=Module["_play"]=(a0,a1,a2,a3,a4,a5,a6)=>(_play=Module["_play"]=wasmExports["W"])(a0,a1,a2,a3,a4,a5,a6);var _stop=Module["_stop"]=a0=>(_stop=Module["_stop"]=wasmExports["X"])(a0);var _disposeSound=Module["_disposeSound"]=a0=>(_disposeSound=Module["_disposeSound"]=wasmExports["Y"])(a0);var _disposeAllSound=Module["_disposeAllSound"]=()=>(_disposeAllSound=Module["_disposeAllSound"]=wasmExports["Z"])();var _getLooping=Module["_getLooping"]=a0=>(_getLooping=Module["_getLooping"]=wasmExports["_"])(a0);var _setLooping=Module["_setLooping"]=(a0,a1)=>(_setLooping=Module["_setLooping"]=wasmExports["$"])(a0,a1);var _getLoopPoint=Module["_getLoopPoint"]=a0=>(_getLoopPoint=Module["_getLoopPoint"]=wasmExports["aa"])(a0);var _setLoopPoint=Module["_setLoopPoint"]=(a0,a1)=>(_setLoopPoint=Module["_setLoopPoint"]=wasmExports["ba"])(a0,a1);var _setVisualizationEnabled=Module["_setVisualizationEnabled"]=a0=>(_setVisualizationEnabled=Module["_setVisualizationEnabled"]=wasmExports["ca"])(a0);var _getVisualizationEnabled=Module["_getVisualizationEnabled"]=()=>(_getVisualizationEnabled=Module["_getVisualizationEnabled"]=wasmExports["da"])();var _getFft=Module["_getFft"]=a0=>(_getFft=Module["_getFft"]=wasmExports["ea"])(a0);var _getWave=Module["_getWave"]=a0=>(_getWave=Module["_getWave"]=wasmExports["fa"])(a0);var _setFftSmoothing=Module["_setFftSmoothing"]=a0=>(_setFftSmoothing=Module["_setFftSmoothing"]=wasmExports["ga"])(a0);var _getAudioTexture=Module["_getAudioTexture"]=a0=>(_getAudioTexture=Module["_getAudioTexture"]=wasmExports["ha"])(a0);var _getAudioTexture2D=Module["_getAudioTexture2D"]=a0=>(_getAudioTexture2D=Module["_getAudioTexture2D"]=wasmExports["ia"])(a0);var _getTextureValue=Module["_getTextureValue"]=(a0,a1)=>(_getTextureValue=Module["_getTextureValue"]=wasmExports["ja"])(a0,a1);var _getLength=Module["_getLength"]=a0=>(_getLength=Module["_getLength"]=wasmExports["ka"])(a0);var _seek=Module["_seek"]=(a0,a1)=>(_seek=Module["_seek"]=wasmExports["la"])(a0,a1);var _getPosition=Module["_getPosition"]=a0=>(_getPosition=Module["_getPosition"]=wasmExports["ma"])(a0);var _getGlobalVolume=Module["_getGlobalVolume"]=()=>(_getGlobalVolume=Module["_getGlobalVolume"]=wasmExports["na"])();var _setGlobalVolume=Module["_setGlobalVolume"]=a0=>(_setGlobalVolume=Module["_setGlobalVolume"]=wasmExports["oa"])(a0);var _getVolume=Module["_getVolume"]=a0=>(_getVolume=Module["_getVolume"]=wasmExports["pa"])(a0);var _setVolume=Module["_setVolume"]=(a0,a1)=>(_setVolume=Module["_setVolume"]=wasmExports["qa"])(a0,a1);var _getPan=Module["_getPan"]=a0=>(_getPan=Module["_getPan"]=wasmExports["ra"])(a0);var _setPan=Module["_setPan"]=(a0,a1)=>(_setPan=Module["_setPan"]=wasmExports["sa"])(a0,a1);var _setPanAbsolute=Module["_setPanAbsolute"]=(a0,a1,a2)=>(_setPanAbsolute=Module["_setPanAbsolute"]=wasmExports["ta"])(a0,a1,a2);var _getIsValidVoiceHandle=Module["_getIsValidVoiceHandle"]=a0=>(_getIsValidVoiceHandle=Module["_getIsValidVoiceHandle"]=wasmExports["ua"])(a0);var _getActiveVoiceCount=Module["_getActiveVoiceCount"]=()=>(_getActiveVoiceCount=Module["_getActiveVoiceCount"]=wasmExports["va"])();var _countAudioSource=Module["_countAudioSource"]=a0=>(_countAudioSource=Module["_countAudioSource"]=wasmExports["wa"])(a0);var _getVoiceCount=Module["_getVoiceCount"]=()=>(_getVoiceCount=Module["_getVoiceCount"]=wasmExports["xa"])();var _getProtectVoice=Module["_getProtectVoice"]=a0=>(_getProtectVoice=Module["_getProtectVoice"]=wasmExports["ya"])(a0);var _setProtectVoice=Module["_setProtectVoice"]=(a0,a1)=>(_setProtectVoice=Module["_setProtectVoice"]=wasmExports["za"])(a0,a1);var _getMaxActiveVoiceCount=Module["_getMaxActiveVoiceCount"]=()=>(_getMaxActiveVoiceCount=Module["_getMaxActiveVoiceCount"]=wasmExports["Aa"])();var _setMaxActiveVoiceCount=Module["_setMaxActiveVoiceCount"]=a0=>(_setMaxActiveVoiceCount=Module["_setMaxActiveVoiceCount"]=wasmExports["Ba"])(a0);var _fadeGlobalVolume=Module["_fadeGlobalVolume"]=(a0,a1)=>(_fadeGlobalVolume=Module["_fadeGlobalVolume"]=wasmExports["Ca"])(a0,a1);var _fadeVolume=Module["_fadeVolume"]=(a0,a1,a2)=>(_fadeVolume=Module["_fadeVolume"]=wasmExports["Da"])(a0,a1,a2);var _fadePan=Module["_fadePan"]=(a0,a1,a2)=>(_fadePan=Module["_fadePan"]=wasmExports["Ea"])(a0,a1,a2);var _fadeRelativePlaySpeed=Module["_fadeRelativePlaySpeed"]=(a0,a1,a2)=>(_fadeRelativePlaySpeed=Module["_fadeRelativePlaySpeed"]=wasmExports["Fa"])(a0,a1,a2);var _schedulePause=Module["_schedulePause"]=(a0,a1)=>(_schedulePause=Module["_schedulePause"]=wasmExports["Ga"])(a0,a1);var _scheduleStop=Module["_scheduleStop"]=(a0,a1)=>(_scheduleStop=Module["_scheduleStop"]=wasmExports["Ha"])(a0,a1);var _oscillateVolume=Module["_oscillateVolume"]=(a0,a1,a2,a3)=>(_oscillateVolume=Module["_oscillateVolume"]=wasmExports["Ia"])(a0,a1,a2,a3);var _oscillatePan=Module["_oscillatePan"]=(a0,a1,a2,a3)=>(_oscillatePan=Module["_oscillatePan"]=wasmExports["Ja"])(a0,a1,a2,a3);var _oscillateRelativePlaySpeed=Module["_oscillateRelativePlaySpeed"]=(a0,a1,a2,a3)=>(_oscillateRelativePlaySpeed=Module["_oscillateRelativePlaySpeed"]=wasmExports["Ka"])(a0,a1,a2,a3);var _oscillateGlobalVolume=Module["_oscillateGlobalVolume"]=(a0,a1,a2)=>(_oscillateGlobalVolume=Module["_oscillateGlobalVolume"]=wasmExports["La"])(a0,a1,a2);var _isFilterActive=Module["_isFilterActive"]=(a0,a1)=>(_isFilterActive=Module["_isFilterActive"]=wasmExports["Ma"])(a0,a1);var _getFilterParamNames=Module["_getFilterParamNames"]=(a0,a1,a2)=>(_getFilterParamNames=Module["_getFilterParamNames"]=wasmExports["Na"])(a0,a1,a2);var _addGlobalFilter=Module["_addGlobalFilter"]=a0=>(_addGlobalFilter=Module["_addGlobalFilter"]=wasmExports["Oa"])(a0);var _removeGlobalFilter=Module["_removeGlobalFilter"]=a0=>(_removeGlobalFilter=Module["_removeGlobalFilter"]=wasmExports["Pa"])(a0);var _setFxParams=Module["_setFxParams"]=(a0,a1,a2)=>(_setFxParams=Module["_setFxParams"]=wasmExports["Qa"])(a0,a1,a2);var _getFxParams=Module["_getFxParams"]=(a0,a1)=>(_getFxParams=Module["_getFxParams"]=wasmExports["Ra"])(a0,a1);var _play3d=Module["_play3d"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)=>(_play3d=Module["_play3d"]=wasmExports["Sa"])(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11);var _set3dSoundSpeed=Module["_set3dSoundSpeed"]=a0=>(_set3dSoundSpeed=Module["_set3dSoundSpeed"]=wasmExports["Ta"])(a0);var _get3dSoundSpeed=Module["_get3dSoundSpeed"]=()=>(_get3dSoundSpeed=Module["_get3dSoundSpeed"]=wasmExports["Ua"])();var _set3dListenerParameters=Module["_set3dListenerParameters"]=(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)=>(_set3dListenerParameters=Module["_set3dListenerParameters"]=wasmExports["Va"])(a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11);var _set3dListenerPosition=Module["_set3dListenerPosition"]=(a0,a1,a2)=>(_set3dListenerPosition=Module["_set3dListenerPosition"]=wasmExports["Wa"])(a0,a1,a2);var _set3dListenerAt=Module["_set3dListenerAt"]=(a0,a1,a2)=>(_set3dListenerAt=Module["_set3dListenerAt"]=wasmExports["Xa"])(a0,a1,a2);var _set3dListenerUp=Module["_set3dListenerUp"]=(a0,a1,a2)=>(_set3dListenerUp=Module["_set3dListenerUp"]=wasmExports["Ya"])(a0,a1,a2);var _set3dListenerVelocity=Module["_set3dListenerVelocity"]=(a0,a1,a2)=>(_set3dListenerVelocity=Module["_set3dListenerVelocity"]=wasmExports["Za"])(a0,a1,a2);var _set3dSourceParameters=Module["_set3dSourceParameters"]=(a0,a1,a2,a3,a4,a5,a6)=>(_set3dSourceParameters=Module["_set3dSourceParameters"]=wasmExports["_a"])(a0,a1,a2,a3,a4,a5,a6);var _set3dSourcePosition=Module["_set3dSourcePosition"]=(a0,a1,a2,a3)=>(_set3dSourcePosition=Module["_set3dSourcePosition"]=wasmExports["$a"])(a0,a1,a2,a3);var _set3dSourceVelocity=Module["_set3dSourceVelocity"]=(a0,a1,a2,a3)=>(_set3dSourceVelocity=Module["_set3dSourceVelocity"]=wasmExports["ab"])(a0,a1,a2,a3);var _set3dSourceMinMaxDistance=Module["_set3dSourceMinMaxDistance"]=(a0,a1,a2)=>(_set3dSourceMinMaxDistance=Module["_set3dSourceMinMaxDistance"]=wasmExports["bb"])(a0,a1,a2);var _set3dSourceAttenuation=Module["_set3dSourceAttenuation"]=(a0,a1,a2)=>(_set3dSourceAttenuation=Module["_set3dSourceAttenuation"]=wasmExports["cb"])(a0,a1,a2);var _set3dSourceDopplerFactor=Module["_set3dSourceDopplerFactor"]=(a0,a1)=>(_set3dSourceDopplerFactor=Module["_set3dSourceDopplerFactor"]=wasmExports["db"])(a0,a1);var _js_init=Module["_js_init"]=a0=>(_js_init=Module["_js_init"]=wasmExports["eb"])(a0);var _js_load=Module["_js_load"]=()=>(_js_load=Module["_js_load"]=wasmExports["fb"])();var _js_play=Module["_js_play"]=a0=>(_js_play=Module["_js_play"]=wasmExports["gb"])(a0);var _js_dispose=Module["_js_dispose"]=()=>(_js_dispose=Module["_js_dispose"]=wasmExports["hb"])();var _listCaptureDevices=Module["_listCaptureDevices"]=(a0,a1,a2)=>(_listCaptureDevices=Module["_listCaptureDevices"]=wasmExports["ib"])(a0,a1,a2);var _freeListCaptureDevices=Module["_freeListCaptureDevices"]=(a0,a1,a2)=>(_freeListCaptureDevices=Module["_freeListCaptureDevices"]=wasmExports["jb"])(a0,a1,a2);var _initCapture=Module["_initCapture"]=a0=>(_initCapture=Module["_initCapture"]=wasmExports["kb"])(a0);var _disposeCapture=Module["_disposeCapture"]=()=>(_disposeCapture=Module["_disposeCapture"]=wasmExports["lb"])();var _isCaptureInited=Module["_isCaptureInited"]=()=>(_isCaptureInited=Module["_isCaptureInited"]=wasmExports["mb"])();var _isCaptureStarted=Module["_isCaptureStarted"]=()=>(_isCaptureStarted=Module["_isCaptureStarted"]=wasmExports["nb"])();var _startCapture=Module["_startCapture"]=()=>(_startCapture=Module["_startCapture"]=wasmExports["ob"])();var _stopCapture=Module["_stopCapture"]=()=>(_stopCapture=Module["_stopCapture"]=wasmExports["pb"])();var _getCaptureFft=Module["_getCaptureFft"]=a0=>(_getCaptureFft=Module["_getCaptureFft"]=wasmExports["qb"])(a0);var _getCaptureWave=Module["_getCaptureWave"]=a0=>(_getCaptureWave=Module["_getCaptureWave"]=wasmExports["rb"])(a0);var _getCaptureTexture=Module["_getCaptureTexture"]=a0=>(_getCaptureTexture=Module["_getCaptureTexture"]=wasmExports["sb"])(a0);var _getCaptureAudioTexture2D=Module["_getCaptureAudioTexture2D"]=a0=>(_getCaptureAudioTexture2D=Module["_getCaptureAudioTexture2D"]=wasmExports["tb"])(a0);var _getCaptureTextureValue=Module["_getCaptureTextureValue"]=(a0,a1)=>(_getCaptureTextureValue=Module["_getCaptureTextureValue"]=wasmExports["ub"])(a0,a1);var _setCaptureFftSmoothing=Module["_setCaptureFftSmoothing"]=a0=>(_setCaptureFftSmoothing=Module["_setCaptureFftSmoothing"]=wasmExports["vb"])(a0);var _emscripten_get_sbrk_ptr=()=>(_emscripten_get_sbrk_ptr=wasmExports["wb"])();var _sbrk=a0=>(_sbrk=wasmExports["xb"])(a0);var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["yb"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["zb"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["Ab"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["Bb"])();var ___cxa_is_pointer_type=a0=>(___cxa_is_pointer_type=wasmExports["Cb"])(a0);var dynCall_iiiji=Module["dynCall_iiiji"]=(a0,a1,a2,a3,a4,a5)=>(dynCall_iiiji=Module["dynCall_iiiji"]=wasmExports["Db"])(a0,a1,a2,a3,a4,a5);var dynCall_jii=Module["dynCall_jii"]=(a0,a1,a2)=>(dynCall_jii=Module["dynCall_jii"]=wasmExports["Eb"])(a0,a1,a2);var dynCall_jiji=Module["dynCall_jiji"]=(a0,a1,a2,a3,a4)=>(dynCall_jiji=Module["dynCall_jiji"]=wasmExports["Fb"])(a0,a1,a2,a3,a4);Module["ccall"]=ccall;Module["cwrap"]=cwrap;var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(){if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run(); diff --git a/web/libflutter_soloud_plugin.wasm b/web/libflutter_soloud_plugin.wasm new file mode 100755 index 0000000..241c595 Binary files /dev/null and b/web/libflutter_soloud_plugin.wasm differ diff --git a/web/src.cmake b/web/src.cmake deleted file mode 100644 index 9cb0119..0000000 --- a/web/src.cmake +++ /dev/null @@ -1,327 +0,0 @@ -set (TARGET_NAME soloud) - -set (HEADER_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../src/soloud/include) -set (SOURCE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../src/soloud/src) - -set (LINK_LIBRARIES) - -# Headers -set (TARGET_HEADERS - ${HEADER_PATH}/soloud.h - ${HEADER_PATH}/soloud_audiosource.h - ${HEADER_PATH}/soloud_ay.h - ${HEADER_PATH}/soloud_bassboostfilter.h - ${HEADER_PATH}/soloud_biquadresonantfilter.h - ${HEADER_PATH}/soloud_bus.h - ${HEADER_PATH}/soloud_dcremovalfilter.h - ${HEADER_PATH}/soloud_echofilter.h - ${HEADER_PATH}/soloud_eqfilter.h - ${HEADER_PATH}/soloud_error.h - ${HEADER_PATH}/soloud_fader.h - ${HEADER_PATH}/soloud_fft.h - ${HEADER_PATH}/soloud_fftfilter.h - ${HEADER_PATH}/soloud_file.h - ${HEADER_PATH}/soloud_file_hack_off.h - ${HEADER_PATH}/soloud_file_hack_on.h - ${HEADER_PATH}/soloud_filter.h - ${HEADER_PATH}/soloud_flangerfilter.h - ${HEADER_PATH}/soloud_freeverbfilter.h - ${HEADER_PATH}/soloud_internal.h - ${HEADER_PATH}/soloud_lofifilter.h - ${HEADER_PATH}/soloud_misc.h - ${HEADER_PATH}/soloud_monotone.h - ${HEADER_PATH}/soloud_noise.h - # ${HEADER_PATH}/soloud_openmpt.h - ${HEADER_PATH}/soloud_queue.h - ${HEADER_PATH}/soloud_robotizefilter.h - ${HEADER_PATH}/soloud_sfxr.h - ${HEADER_PATH}/soloud_speech.h - ${HEADER_PATH}/soloud_tedsid.h - ${HEADER_PATH}/soloud_thread.h - ${HEADER_PATH}/soloud_vic.h - ${HEADER_PATH}/soloud_vizsn.h - ${HEADER_PATH}/soloud_wav.h - ${HEADER_PATH}/soloud_waveshaperfilter.h - ${HEADER_PATH}/soloud_wavstream.h -) - - -# Core -set (CORE_PATH ${SOURCE_PATH}/core) -set (CORE_SOURCES - ${CORE_PATH}/soloud.cpp - ${CORE_PATH}/soloud_audiosource.cpp - ${CORE_PATH}/soloud_bus.cpp - ${CORE_PATH}/soloud_core_3d.cpp - ${CORE_PATH}/soloud_core_basicops.cpp - ${CORE_PATH}/soloud_core_faderops.cpp - ${CORE_PATH}/soloud_core_filterops.cpp - ${CORE_PATH}/soloud_core_getters.cpp - ${CORE_PATH}/soloud_core_setters.cpp - ${CORE_PATH}/soloud_core_voicegroup.cpp - ${CORE_PATH}/soloud_core_voiceops.cpp - ${CORE_PATH}/soloud_fader.cpp - ${CORE_PATH}/soloud_fft.cpp - ${CORE_PATH}/soloud_fft_lut.cpp - ${CORE_PATH}/soloud_file.cpp - ${CORE_PATH}/soloud_filter.cpp - ${CORE_PATH}/soloud_misc.cpp - ${CORE_PATH}/soloud_queue.cpp - ${CORE_PATH}/soloud_thread.cpp -) - - -# Audiosources -set (AUDIOSOURCES_PATH ${SOURCE_PATH}/audiosource) -set (AUDIOSOURCES_SOURCES) -if(WIN32) - # openmpt only in Windows - set (AUDIOSOURCES_SOURCES - ${AUDIOSOURCES_PATH}/openmpt/soloud_openmpt.cpp - ${AUDIOSOURCES_PATH}/openmpt/soloud_openmpt_dll.c - ) -endif() -set (AUDIOSOURCES_SOURCES - ${AUDIOSOURCES_SOURCES} - # ay - ${AUDIOSOURCES_PATH}/ay/chipplayer.cpp - # ${AUDIOSOURCES_PATH}/ay/chipplayer.h - # ${AUDIOSOURCES_PATH}/ay/readme.txt - ${AUDIOSOURCES_PATH}/ay/sndbuffer.cpp - # ${AUDIOSOURCES_PATH}/ay/sndbuffer.h - ${AUDIOSOURCES_PATH}/ay/sndchip.cpp - # ${AUDIOSOURCES_PATH}/ay/sndchip.h - ${AUDIOSOURCES_PATH}/ay/sndrender.cpp - # ${AUDIOSOURCES_PATH}/ay/sndrender.h - ${AUDIOSOURCES_PATH}/ay/soloud_ay.cpp - - # monotone - ${AUDIOSOURCES_PATH}/monotone/soloud_monotone.cpp - - # noise - ${AUDIOSOURCES_PATH}/noise/soloud_noise.cpp - - # sfxr - ${AUDIOSOURCES_PATH}/sfxr/soloud_sfxr.cpp - - # speech - # ${AUDIOSOURCES_PATH}/speech/Elements.def - ${AUDIOSOURCES_PATH}/speech/darray.cpp - # ${AUDIOSOURCES_PATH}/speech/darray.h - ${AUDIOSOURCES_PATH}/speech/klatt.cpp - # ${AUDIOSOURCES_PATH}/speech/klatt.h - ${AUDIOSOURCES_PATH}/speech/resonator.cpp - # ${AUDIOSOURCES_PATH}/speech/resonator.h - ${AUDIOSOURCES_PATH}/speech/soloud_speech.cpp - ${AUDIOSOURCES_PATH}/speech/tts.cpp - # ${AUDIOSOURCES_PATH}/speech/tts.h - - # tedsid - ${AUDIOSOURCES_PATH}/tedsid/sid.cpp - # ${AUDIOSOURCES_PATH}/tedsid/sid.h - ${AUDIOSOURCES_PATH}/tedsid/soloud_tedsid.cpp - ${AUDIOSOURCES_PATH}/tedsid/ted.cpp - # ${AUDIOSOURCES_PATH}/tedsid/ted.h - - # vic - ${AUDIOSOURCES_PATH}/vic/soloud_vic.cpp - - # vizsn - ${AUDIOSOURCES_PATH}/vizsn/soloud_vizsn.cpp - - # wav - # ${AUDIOSOURCES_PATH}/wav/dr_flac.h - ${AUDIOSOURCES_PATH}/wav/dr_impl.cpp - # ${AUDIOSOURCES_PATH}/wav/dr_mp3.h - # ${AUDIOSOURCES_PATH}/wav/dr_wav.h - ${AUDIOSOURCES_PATH}/wav/soloud_wav.cpp - ${AUDIOSOURCES_PATH}/wav/soloud_wavstream.cpp - ${AUDIOSOURCES_PATH}/wav/stb_vorbis.c - # ${AUDIOSOURCES_PATH}/wav/stb_vorbis.h -) - - - -# Backends -# TODO: Other backends -set (BACKENDS_PATH ${SOURCE_PATH}/backend) -set (BACKENDS_SOURCES) - -if (SOLOUD_BACKEND_NULL) - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/null/soloud_null.cpp - ) - add_definitions(-DWITH_NULL) -endif() - -if (SOLOUD_BACKEND_SDL2) - find_package (SDL2 REQUIRED) - include_directories (${SDL2_INCLUDE_DIR}) - add_definitions (-DWITH_SDL2_STATIC) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/sdl2_static/soloud_sdl2_static.cpp - ) - - set (LINK_LIBRARIES - ${LINK_LIBRARIES} - ${SDL2_LIBRARY} - ) - -endif() - -if (SOLOUD_BACKEND_ALSA) - add_definitions (-DWITH_ALSA) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/alsa/soloud_alsa.cpp - ) - - find_library (ALSA_LIBRARY asound) - set (LINK_LIBRARIES - ${LINK_LIBRARIES} - ${ALSA_LIBRARY} - ) -endif() - - -if (SOLOUD_BACKEND_COREAUDIO) - if (NOT APPLE) - message (FATAL_ERROR "CoreAudio backend can be enabled only on Apple!") - endif () - - add_definitions (-DWITH_COREAUDIO) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/coreaudio/soloud_coreaudio.cpp - ) - - find_library (AUDIOTOOLBOX_FRAMEWORK AudioToolbox) - set (LINK_LIBRARIES - ${LINK_LIBRARIES} - ${AUDIOTOOLBOX_FRAMEWORK} - ) -endif() - - -if (SOLOUD_BACKEND_OPENSLES) - add_definitions (-DWITH_OPENSLES) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/opensles/soloud_opensles.cpp - ) - - find_library (OPENSLES_LIBRARY OpenSLES) - set (LINK_LIBRARIES - ${LINK_LIBRARIES} - ${OPENSLES_LIBRARY} - ) -endif() - - -if (SOLOUD_BACKEND_XAUDIO2) - add_definitions (-DWITH_XAUDIO2) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/xaudio2/soloud_xaudio2.cpp - ) -endif() - -if (SOLOUD_BACKEND_WINMM) - add_definitions (-DWITH_WINMM) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/winmm/soloud_winmm.cpp - ) -endif() - -if (SOLOUD_BACKEND_WASAPI) - add_definitions (-DWITH_WASAPI) - - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/wasapi/soloud_wasapi.cpp - ) -endif() - -if (SOLOUD_BACKEND_MINIAUDIO) - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/miniaudio/soloud_miniaudio.cpp - ) - - add_definitions(-DWITH_MINIAUDIO) - set (BACKENDS_SOURCES - ${BACKENDS_SOURCES} - ${BACKENDS_PATH}/miniaudio/miniaudio.h - ) -endif() - - -# Filters -set (FILTERS_PATH ${SOURCE_PATH}/filter) -set (FILTERS_SOURCES - ${FILTERS_PATH}/soloud_bassboostfilter.cpp - ${FILTERS_PATH}/soloud_biquadresonantfilter.cpp - ${FILTERS_PATH}/soloud_dcremovalfilter.cpp - ${FILTERS_PATH}/soloud_echofilter.cpp - ${FILTERS_PATH}/soloud_eqfilter.cpp - ${FILTERS_PATH}/soloud_fftfilter.cpp - ${FILTERS_PATH}/soloud_flangerfilter.cpp - ${FILTERS_PATH}/soloud_freeverbfilter.cpp - ${FILTERS_PATH}/soloud_lofifilter.cpp - ${FILTERS_PATH}/soloud_robotizefilter.cpp - ${FILTERS_PATH}/soloud_waveshaperfilter.cpp -) - - -# All together -source_group ("Includes" FILES ${TARGET_HEADERS}) -source_group ("Core" FILES ${CORE_SOURCES}) -source_group ("Audiosources" FILES ${AUDIOSOURCES_SOURCES}) -source_group ("Backends" FILES ${BACKENDS_SOURCES}) -source_group ("Filters" FILES ${FILTERS_SOURCES}) - -set (TARGET_SOURCES - ${CORE_SOURCES} - ${AUDIOSOURCES_SOURCES} - ${BACKENDS_SOURCES} - ${FILTERS_SOURCES} -) - -if (SOLOUD_C_API) - set (TARGET_SOURCES - ${TARGET_SOURCES} - ${SOURCE_PATH}/c_api/soloud.def - ${SOURCE_PATH}/c_api/soloud_c.cpp - ) - set (TARGET_HEADERS - ${TARGET_HEADERS} - ${HEADER_PATH}/soloud_c.h - ) -endif() - -# if (SOLOUD_DYNAMIC) -# add_library(${TARGET_NAME} SHARED ${TARGET_SOURCES}) -# endif () - -# if (SOLOUD_STATIC) -# add_library(${TARGET_NAME} STATIC ${TARGET_SOURCES}) -# endif() - -# target_link_libraries (${TARGET_NAME} ${LINK_LIBRARIES}) - - -# force stb_vorbis.c to be treated as c++ -set_source_files_properties(${AUDIOSOURCES_PATH}/wav/stb_vorbis.c PROPERTIES LANGUAGE CXX ) -# target_compile_options(${TARGET_NAME} PRIVATE -Wall -Wno-error -ldl -fPIC -O2 -DNDEBUG -fno-exceptions -fno-rtti) # -lpthread -ldl - -# include (${CMAKE_CURRENT_SOURCE_DIR}/src/soloud/contrib/cmake/Install.cmake) -# INSTALL(FILES ${TARGET_HEADERS} DESTINATION ${CMAKE_CURRENT_SOURCE_DIR}/src/soloud/lib/${TARGET_NAME}) diff --git a/web/worker.dart b/web/worker.dart new file mode 100644 index 0000000..f83292e --- /dev/null +++ b/web/worker.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:convert' show jsonDecode; +import 'dart:js_interop'; +import 'dart:js_interop_unsafe'; + +import 'package:web/web.dart' as web; + +// Masked type: ServiceWorkerGlobalScope +@JS('self') +external JSObject get globalScopeSelf; + +@JS('self.importScript') +// ignore: unused_element +external JSAny _importScript(String path); + +void jsSendMessage(dynamic m) { + globalContext.callMethod('postMessage'.toJS, (m as Object).jsify()); +} + +Stream callbackToStream( + JSObject object, + String name, + T Function(J jsValue) unwrapValue, +) { + final controller = StreamController.broadcast(sync: true); + + void eventFunction(JSAny event) { + controller.add(unwrapValue(event as J)); + } + + object.setProperty( + name.toJS, + eventFunction.toJS, + ); + return controller.stream; +} + +class Worker { + Worker() { + _outputController = StreamController(); + callbackToStream(globalScopeSelf, 'onmessage', (web.MessageEvent e) { + final data = (e as JSObject).getProperty('data'.toJS); + _outputController.add(data); + }); + } + late StreamController _outputController; + + Stream onReceive() => _outputController.stream; + + void sendMessage(dynamic message) { + jsSendMessage(message); + } +} + +/// The main Web Worker +void main() async { + // print('Worker created.\n'); + final worker = Worker(); + worker.onReceive().listen((data) { + // print('Dart worker: ' + // 'onMessage received $data with type of ${data.runtimeType}\n'); + + if (data is String) { + try { + final parseMap = jsonDecode(data) as Map; + // ignore: avoid_print + print('Received $data PARSED TO $parseMap\n'); + if (parseMap['message'] == 'voiceEndedCallback') { + worker.sendMessage(data); + } + } catch (e) { + // ignore: avoid_print + print("Received data from WASM worker but it's not a String!\n"); + } + } + }); +} diff --git a/web/worker.dart.js b/web/worker.dart.js new file mode 100644 index 0000000..1ed270a --- /dev/null +++ b/web/worker.dart.js @@ -0,0 +1,3251 @@ +(function dartProgram(){function copyProperties(a,b){var s=Object.keys(a) +for(var r=0;r=0)return true +if(typeof version=="function"&&version.length==0){var q=version() +if(/^\d+\.\d+\.\d+\.\d+$/.test(q))return true}}catch(p){}return false}() +function inherit(a,b){a.prototype.constructor=a +a.prototype["$i"+a.name]=a +if(b!=null){if(z){Object.setPrototypeOf(a.prototype,b.prototype) +return}var s=Object.create(b.prototype) +copyProperties(a.prototype,s) +a.prototype=s}}function inheritMany(a,b){for(var s=0;s>>0===b&&b").A(d).i("ar<1,2>")) +return new A.a1(a,b,c.i("@<0>").A(d).i("a1<1,2>"))}, +aA:function aA(a){this.a=a}, +e:function e(){}, +C:function C(){}, +a9:function a9(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +a1:function a1(a,b,c){this.a=a +this.b=b +this.$ti=c}, +ar:function ar(a,b,c){this.a=a +this.b=b +this.$ti=c}, +bp:function bp(a,b,c){var _=this +_.a=null +_.b=a +_.c=b +_.$ti=c}, +G:function G(a,b,c){this.a=a +this.b=b +this.$ti=c}, +at:function at(){}, +T:function T(a){this.a=a}, +eD(a){var s=v.mangledGlobalNames[a] +if(s!=null)return s +return"minified:"+a}, +hB(a,b){var s +if(b!=null){s=b.x +if(s!=null)return s}return t.p.b(a)}, +n(a){var s +if(typeof a=="string")return a +if(typeof a=="number"){if(a!==0)return""+a}else if(!0===a)return"true" +else if(!1===a)return"false" +else if(a==null)return"null" +s=J.bb(a) +return s}, +aH(a){var s,r=$.dX +if(r==null)r=$.dX=Symbol("identityHashCode") +s=a[r] +if(s==null){s=Math.random()*0x3fffffff|0 +a[r]=s}return s}, +cl(a){return A.f9(a)}, +f9(a){var s,r,q,p +if(a instanceof A.d)return A.v(A.am(a),null) +s=J.M(a) +if(s===B.u||s===B.x||t.o.b(a)){r=B.d(a) +if(r!=="Object"&&r!=="")return r +q=a.constructor +if(typeof q=="function"){p=q.name +if(typeof p=="string"&&p!=="Object"&&p!=="")return p}}return A.v(A.am(a),null)}, +fc(a){if(typeof a=="number"||A.d4(a))return J.bb(a) +if(typeof a=="string")return JSON.stringify(a) +if(a instanceof A.a_)return a.h(0) +return"Instance of '"+A.cl(a)+"'"}, +S(a,b,c){var s,r,q={} +q.a=0 +s=[] +r=[] +q.a=b.length +B.b.a2(s,b) +q.b="" +if(c!=null&&c.a!==0)c.t(0,new A.ck(q,r,s)) +return J.eS(a,new A.cb(B.A,0,s,r,0))}, +fa(a,b,c){var s,r,q +if(Array.isArray(b))s=c==null||c.a===0 +else s=!1 +if(s){r=b.length +if(r===0){if(!!a.$0)return a.$0()}else if(r===1){if(!!a.$1)return a.$1(b[0])}else if(r===2){if(!!a.$2)return a.$2(b[0],b[1])}else if(r===3){if(!!a.$3)return a.$3(b[0],b[1],b[2])}else if(r===4){if(!!a.$4)return a.$4(b[0],b[1],b[2],b[3])}else if(r===5)if(!!a.$5)return a.$5(b[0],b[1],b[2],b[3],b[4]) +q=a[""+"$"+r] +if(q!=null)return q.apply(a,b)}return A.f8(a,b,c)}, +f8(a,b,c){var s,r,q,p,o,n,m,l,k,j,i,h,g=Array.isArray(b)?b:A.dq(b,t.z),f=g.length,e=a.$R +if(fn)return A.S(a,g,null) +if(fe)return A.S(a,g,c) +if(g===b)g=A.dq(g,t.z) +l=Object.keys(q) +if(c==null)for(r=l.length,k=0;k=s)return A.dT(b,s,a,r) +return new A.aI(null,null,!0,b,r,"Value not in range")}, +b(a){return A.ey(new Error(),a)}, +ey(a,b){var s +if(b==null)b=new A.I() +a.dartException=b +s=A.hK +if("defineProperty" in Object){Object.defineProperty(a,"message",{get:s}) +a.name=""}else a.toString=s +return a}, +hK(){return J.bb(this.dartException)}, +ba(a){throw A.b(a)}, +eB(a,b){throw A.ey(b,a)}, +dJ(a){throw A.b(A.a0(a))}, +J(a){var s,r,q,p,o,n +a=A.hI(a.replace(String({}),"$receiver$")) +s=a.match(/\\\$[a-zA-Z]+\\\$/g) +if(s==null)s=A.W([],t.s) +r=s.indexOf("\\$arguments\\$") +q=s.indexOf("\\$argumentsExpr\\$") +p=s.indexOf("\\$expr\\$") +o=s.indexOf("\\$method\\$") +n=s.indexOf("\\$receiver\\$") +return new A.cq(a.replace(new RegExp("\\\\\\$arguments\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$argumentsExpr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$expr\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$method\\\\\\$","g"),"((?:x|[^x])*)").replace(new RegExp("\\\\\\$receiver\\\\\\$","g"),"((?:x|[^x])*)"),r,q,p,o,n)}, +cr(a){return function($expr$){var $argumentsExpr$="$arguments$" +try{$expr$.$method$($argumentsExpr$)}catch(s){return s.message}}(a)}, +e1(a){return function($expr$){try{$expr$.$method$}catch(s){return s.message}}(a)}, +dp(a,b){var s=b==null,r=s?null:b.method +return new A.bn(a,r,s?null:b.receiver)}, +O(a){if(a==null)return new A.cj(a) +if(a instanceof A.as)return A.Z(a,a.a) +if(typeof a!=="object")return a +if("dartException" in a)return A.Z(a,a.dartException) +return A.hi(a)}, +Z(a,b){if(t.Q.b(b))if(b.$thrownJsError==null)b.$thrownJsError=a +return b}, +hi(a){var s,r,q,p,o,n,m,l,k,j,i,h,g +if(!("message" in a))return a +s=a.message +if("number" in a&&typeof a.number=="number"){r=a.number +q=r&65535 +if((B.v.aS(r,16)&8191)===10)switch(q){case 438:return A.Z(a,A.dp(A.n(s)+" (Error "+q+")",null)) +case 445:case 5007:A.n(s) +return A.Z(a,new A.aG())}}if(a instanceof TypeError){p=$.eE() +o=$.eF() +n=$.eG() +m=$.eH() +l=$.eK() +k=$.eL() +j=$.eJ() +$.eI() +i=$.eN() +h=$.eM() +g=p.u(s) +if(g!=null)return A.Z(a,A.dp(s,g)) +else{g=o.u(s) +if(g!=null){g.method="call" +return A.Z(a,A.dp(s,g))}else if(n.u(s)!=null||m.u(s)!=null||l.u(s)!=null||k.u(s)!=null||j.u(s)!=null||m.u(s)!=null||i.u(s)!=null||h.u(s)!=null)return A.Z(a,new A.aG())}return A.Z(a,new A.bE(typeof s=="string"?s:""))}if(a instanceof RangeError){if(typeof s=="string"&&s.indexOf("call stack")!==-1)return new A.aJ() +s=function(b){try{return String(b)}catch(f){}return null}(a) +return A.Z(a,new A.P(!1,null,null,typeof s=="string"?s.replace(/^RangeError:\s*/,""):s))}if(typeof InternalError=="function"&&a instanceof InternalError)if(typeof s=="string"&&s==="too much recursion")return new A.aJ() +return a}, +Y(a){var s +if(a instanceof A.as)return a.b +if(a==null)return new A.b_(a) +s=a.$cachedTrace +if(s!=null)return s +s=new A.b_(a) +if(typeof a==="object")a.$cachedTrace=s +return s}, +dG(a){if(a==null)return J.dj(a) +if(typeof a=="object")return A.aH(a) +return J.dj(a)}, +fV(a,b,c,d,e,f){switch(b){case 0:return a.$0() +case 1:return a.$1(c) +case 2:return a.$2(c,d) +case 3:return a.$3(c,d,e) +case 4:return a.$4(c,d,e,f)}throw A.b(new A.cC("Unsupported number of arguments for wrapped closure"))}, +d9(a,b){var s=a.$identity +if(!!s)return s +s=A.hr(a,b) +a.$identity=s +return s}, +hr(a,b){var s +switch(b){case 0:s=a.$0 +break +case 1:s=a.$1 +break +case 2:s=a.$2 +break +case 3:s=a.$3 +break +case 4:s=a.$4 +break +default:s=null}if(s!=null)return s.bind(a) +return function(c,d,e){return function(f,g,h,i){return e(c,d,f,g,h,i)}}(a,b,A.fV)}, +f_(a2){var s,r,q,p,o,n,m,l,k,j,i=a2.co,h=a2.iS,g=a2.iI,f=a2.nDA,e=a2.aI,d=a2.fs,c=a2.cs,b=d[0],a=c[0],a0=i[b],a1=a2.fT +a1.toString +s=h?Object.create(new A.cm().constructor.prototype):Object.create(new A.an(null,null).constructor.prototype) +s.$initialize=s.constructor +r=h?function static_tear_off(){this.$initialize()}:function tear_off(a3,a4){this.$initialize(a3,a4)} +s.constructor=r +r.prototype=s +s.$_name=b +s.$_target=a0 +q=!h +if(q)p=A.dS(b,a0,g,f) +else{s.$static_name=b +p=a0}s.$S=A.eW(a1,h,g) +s[a]=p +for(o=p,n=1;n>>0!==a||a>=c)throw A.b(A.dz(b,a))}, +bq:function bq(){}, +aE:function aE(){}, +br:function br(){}, +aa:function aa(){}, +aC:function aC(){}, +aD:function aD(){}, +bs:function bs(){}, +bt:function bt(){}, +bu:function bu(){}, +bv:function bv(){}, +bw:function bw(){}, +bx:function bx(){}, +by:function by(){}, +aF:function aF(){}, +bz:function bz(){}, +aV:function aV(){}, +aW:function aW(){}, +aX:function aX(){}, +aY:function aY(){}, +dY(a,b){var s=b.c +return s==null?b.c=A.dv(a,b.x,!0):s}, +dr(a,b){var s=b.c +return s==null?b.c=A.b4(a,"a7",[b.x]):s}, +dZ(a){var s=a.w +if(s===6||s===7||s===8)return A.dZ(a.x) +return s===12||s===13}, +fe(a){return a.as}, +dA(a){return A.bU(v.typeUniverse,a,!1)}, +X(a1,a2,a3,a4){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0=a2.w +switch(a0){case 5:case 1:case 2:case 3:case 4:return a2 +case 6:s=a2.x +r=A.X(a1,s,a3,a4) +if(r===s)return a2 +return A.eg(a1,r,!0) +case 7:s=a2.x +r=A.X(a1,s,a3,a4) +if(r===s)return a2 +return A.dv(a1,r,!0) +case 8:s=a2.x +r=A.X(a1,s,a3,a4) +if(r===s)return a2 +return A.ee(a1,r,!0) +case 9:q=a2.y +p=A.ak(a1,q,a3,a4) +if(p===q)return a2 +return A.b4(a1,a2.x,p) +case 10:o=a2.x +n=A.X(a1,o,a3,a4) +m=a2.y +l=A.ak(a1,m,a3,a4) +if(n===o&&l===m)return a2 +return A.dt(a1,n,l) +case 11:k=a2.x +j=a2.y +i=A.ak(a1,j,a3,a4) +if(i===j)return a2 +return A.ef(a1,k,i) +case 12:h=a2.x +g=A.X(a1,h,a3,a4) +f=a2.y +e=A.hf(a1,f,a3,a4) +if(g===h&&e===f)return a2 +return A.ed(a1,g,e) +case 13:d=a2.y +a4+=d.length +c=A.ak(a1,d,a3,a4) +o=a2.x +n=A.X(a1,o,a3,a4) +if(c===d&&n===o)return a2 +return A.du(a1,n,c,!0) +case 14:b=a2.x +if(b=0)p+=" "+r[q];++q}return p+"})"}, +el(a4,a5,a6){var s,r,q,p,o,n,m,l,k,j,i,h,g,f,e,d,c,b,a,a0,a1,a2,a3=", " +if(a6!=null){s=a6.length +if(a5==null){a5=A.W([],t.s) +r=null}else r=a5.length +q=a5.length +for(p=s;p>0;--p)a5.push("T"+(q+p)) +for(o=t.X,n=t._,m="<",l="",p=0;p=0))return A.B(a5,j) +m=B.h.au(m+l,a5[j]) +i=a6[p] +h=i.w +if(!(h===2||h===3||h===4||h===5||i===o))k=i===n +else k=!0 +if(!k)m+=" extends "+A.v(i,a5)}m+=">"}else{m="" +r=null}o=a4.x +g=a4.y +f=g.a +e=f.length +d=g.b +c=d.length +b=g.c +a=b.length +a0=A.v(o,a5) +for(a1="",a2="",p=0;p0){a1+=a2+"[" +for(a2="",p=0;p0){a1+=a2+"{" +for(a2="",p=0;p "+a0}, +v(a,b){var s,r,q,p,o,n,m,l=a.w +if(l===5)return"erased" +if(l===2)return"dynamic" +if(l===3)return"void" +if(l===1)return"Never" +if(l===4)return"any" +if(l===6)return A.v(a.x,b) +if(l===7){s=a.x +r=A.v(s,b) +q=s.w +return(q===12||q===13?"("+r+")":r)+"?"}if(l===8)return"FutureOr<"+A.v(a.x,b)+">" +if(l===9){p=A.hh(a.x) +o=a.y +return o.length>0?p+("<"+A.es(o,b)+">"):p}if(l===11)return A.h9(a,b) +if(l===12)return A.el(a,b,null) +if(l===13)return A.el(a.x,b,a.y) +if(l===14){n=a.x +m=b.length +n=m-1-n +if(!(n>=0&&n0)p+="<"+A.b3(c)+">" +s=a.eC.get(p) +if(s!=null)return s +r=new A.z(null,null) +r.w=9 +r.x=b +r.y=c +if(c.length>0)r.c=c[0] +r.as=p +q=A.K(a,r) +a.eC.set(p,q) +return q}, +dt(a,b,c){var s,r,q,p,o,n +if(b.w===10){s=b.x +r=b.y.concat(c)}else{r=c +s=b}q=s.as+(";<"+A.b3(r)+">") +p=a.eC.get(q) +if(p!=null)return p +o=new A.z(null,null) +o.w=10 +o.x=s +o.y=r +o.as=q +n=A.K(a,o) +a.eC.set(q,n) +return n}, +ef(a,b,c){var s,r,q="+"+(b+"("+A.b3(c)+")"),p=a.eC.get(q) +if(p!=null)return p +s=new A.z(null,null) +s.w=11 +s.x=b +s.y=c +s.as=q +r=A.K(a,s) +a.eC.set(q,r) +return r}, +ed(a,b,c){var s,r,q,p,o,n=b.as,m=c.a,l=m.length,k=c.b,j=k.length,i=c.c,h=i.length,g="("+A.b3(m) +if(j>0){s=l>0?",":"" +g+=s+"["+A.b3(k)+"]"}if(h>0){s=l>0?",":"" +g+=s+"{"+A.fv(i)+"}"}r=n+(g+")") +q=a.eC.get(r) +if(q!=null)return q +p=new A.z(null,null) +p.w=12 +p.x=b +p.y=c +p.as=r +o=A.K(a,p) +a.eC.set(r,o) +return o}, +du(a,b,c,d){var s,r=b.as+("<"+A.b3(c)+">"),q=a.eC.get(r) +if(q!=null)return q +s=A.fx(a,b,c,r,d) +a.eC.set(r,s) +return s}, +fx(a,b,c,d,e){var s,r,q,p,o,n,m,l +if(e){s=c.length +r=A.d_(s) +for(q=0,p=0;p0){n=A.X(a,b,r,0) +m=A.ak(a,c,r,0) +return A.du(a,n,m,c!==m)}}l=new A.z(null,null) +l.w=13 +l.x=b +l.y=c +l.as=d +return A.K(a,l)}, +e9(a,b,c,d){return{u:a,e:b,r:c,s:[],p:0,n:d}}, +eb(a){var s,r,q,p,o,n,m,l=a.r,k=a.s +for(s=l.length,r=0;r=48&&q<=57)r=A.fo(r+1,q,l,k) +else if((((q|32)>>>0)-97&65535)<26||q===95||q===36||q===124)r=A.ea(a,r,l,k,!1) +else if(q===46)r=A.ea(a,r,l,k,!0) +else{++r +switch(q){case 44:break +case 58:k.push(!1) +break +case 33:k.push(!0) +break +case 59:k.push(A.V(a.u,a.e,k.pop())) +break +case 94:k.push(A.fA(a.u,k.pop())) +break +case 35:k.push(A.b5(a.u,5,"#")) +break +case 64:k.push(A.b5(a.u,2,"@")) +break +case 126:k.push(A.b5(a.u,3,"~")) +break +case 60:k.push(a.p) +a.p=k.length +break +case 62:A.fq(a,k) +break +case 38:A.fp(a,k) +break +case 42:p=a.u +k.push(A.eg(p,A.V(p,a.e,k.pop()),a.n)) +break +case 63:p=a.u +k.push(A.dv(p,A.V(p,a.e,k.pop()),a.n)) +break +case 47:p=a.u +k.push(A.ee(p,A.V(p,a.e,k.pop()),a.n)) +break +case 40:k.push(-3) +k.push(a.p) +a.p=k.length +break +case 41:A.fn(a,k) +break +case 91:k.push(a.p) +a.p=k.length +break +case 93:o=k.splice(a.p) +A.ec(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-1) +break +case 123:k.push(a.p) +a.p=k.length +break +case 125:o=k.splice(a.p) +A.fs(a.u,a.e,o) +a.p=k.pop() +k.push(o) +k.push(-2) +break +case 43:n=l.indexOf("(",r) +k.push(l.substring(r,n)) +k.push(-4) +k.push(a.p) +a.p=k.length +r=n+1 +break +default:throw"Bad character "+q}}}m=k.pop() +return A.V(a.u,a.e,m)}, +fo(a,b,c,d){var s,r,q=b-48 +for(s=c.length;a=48&&r<=57))break +q=q*10+(r-48)}d.push(q) +return a}, +ea(a,b,c,d,e){var s,r,q,p,o,n,m=b+1 +for(s=c.length;m>>0)-97&65535)<26||r===95||r===36||r===124))q=r>=48&&r<=57 +else q=!0 +if(!q)break}}p=c.substring(b,m) +if(e){s=a.u +o=a.e +if(o.w===10)o=o.x +n=A.fF(s,o.x)[p] +if(n==null)A.ba('No "'+p+'" in "'+A.fe(o)+'"') +d.push(A.cZ(s,o,n))}else d.push(p) +return m}, +fq(a,b){var s,r=a.u,q=A.e8(a,b),p=b.pop() +if(typeof p=="string")b.push(A.b4(r,p,q)) +else{s=A.V(r,a.e,p) +switch(s.w){case 12:b.push(A.du(r,s,q,a.n)) +break +default:b.push(A.dt(r,s,q)) +break}}}, +fn(a,b){var s,r,q,p,o,n=null,m=a.u,l=b.pop() +if(typeof l=="number")switch(l){case-1:s=b.pop() +r=n +break +case-2:r=b.pop() +s=n +break +default:b.push(l) +r=n +s=r +break}else{b.push(l) +r=n +s=r}q=A.e8(a,b) +l=b.pop() +switch(l){case-3:l=b.pop() +if(s==null)s=m.sEA +if(r==null)r=m.sEA +p=A.V(m,a.e,l) +o=new A.bM() +o.a=q +o.b=s +o.c=r +b.push(A.ed(m,p,o)) +return +case-4:b.push(A.ef(m,b.pop(),q)) +return +default:throw A.b(A.bd("Unexpected state under `()`: "+A.n(l)))}}, +fp(a,b){var s=b.pop() +if(0===s){b.push(A.b5(a.u,1,"0&")) +return}if(1===s){b.push(A.b5(a.u,4,"1&")) +return}throw A.b(A.bd("Unexpected extended operation "+A.n(s)))}, +e8(a,b){var s=b.splice(a.p) +A.ec(a.u,a.e,s) +a.p=b.pop() +return s}, +V(a,b,c){if(typeof c=="string")return A.b4(a,c,a.sEA) +else if(typeof c=="number"){b.toString +return A.fr(a,b,c)}else return c}, +ec(a,b,c){var s,r=c.length +for(s=0;sn)return!1 +m=n-o +l=s.b +k=r.b +j=l.length +i=k.length +if(o+j=d)return!1 +a1=f[b] +b+=3 +if(a00?new Array(q):v.typeUniverse.sEA +for(o=0;o0?new Array(a):v.typeUniverse.sEA}, +z:function z(a,b){var _=this +_.a=a +_.b=b +_.r=_.f=_.d=_.c=null +_.w=0 +_.as=_.Q=_.z=_.y=_.x=null}, +bM:function bM(){this.c=this.b=this.a=null}, +cY:function cY(a){this.a=a}, +bL:function bL(){}, +b2:function b2(a){this.a=a}, +fg(){var s,r,q={} +if(self.scheduleImmediate!=null)return A.hl() +if(self.MutationObserver!=null&&self.document!=null){s=self.document.createElement("div") +r=self.document.createElement("span") +q.a=null +new self.MutationObserver(A.d9(new A.cz(q),1)).observe(s,{childList:true}) +return new A.cy(q,s,r)}else if(self.setImmediate!=null)return A.hm() +return A.hn()}, +fh(a){self.scheduleImmediate(A.d9(new A.cA(a),0))}, +fi(a){self.setImmediate(A.d9(new A.cB(a),0))}, +fj(a){A.ft(0,a)}, +ft(a,b){var s=new A.cW() +s.aA(a,b) +return s}, +h5(a){return new A.bG(new A.o($.j,a.i("o<0>")),a.i("bG<0>"))}, +fK(a,b){a.$2(0,null) +b.b=!0 +return b.a}, +ie(a,b){A.fL(a,b)}, +fJ(a,b){var s,r=a==null?b.$ti.c.a(a):a +if(!b.b)b.a.aa(r) +else{s=b.a +if(b.$ti.i("a7<1>").b(r))s.ac(r) +else s.S(r)}}, +fI(a,b){var s=A.O(a),r=A.Y(a),q=b.a +if(b.b)q.D(s,r) +else q.aC(s,r)}, +fL(a,b){var s,r,q=new A.d1(b),p=new A.d2(b) +if(a instanceof A.o)a.al(q,p,t.z) +else{s=t.z +if(a instanceof A.o)a.a6(q,p,s) +else{r=new A.o($.j,t.h) +r.a=8 +r.c=a +r.al(q,p,s)}}}, +hj(a){var s=function(b,c){return function(d,e){while(true){try{b(d,e) +break}catch(r){e=r +d=c}}}}(a,1) +return $.j.a4(new A.d6(s))}, +c2(a,b){var s=A.d8(a,"error",t.K) +return new A.be(s,b==null?A.eT(a):b)}, +eT(a){var s +if(t.Q.b(a)){s=a.gO() +if(s!=null)return s}return B.t}, +e5(a,b){var s,r +for(;s=a.a,(s&4)!==0;)a=a.c +s|=b.a&1 +a.a=s +if((s&24)!==0){r=b.K() +b.I(a) +A.ah(b,r)}else{r=b.c +b.aj(a) +a.a0(r)}}, +fl(a,b){var s,r,q={},p=q.a=a +for(;s=p.a,(s&4)!==0;){p=p.c +q.a=p}if((s&24)===0){r=b.c +b.aj(p) +q.a.a0(r) +return}if((s&16)===0&&b.c==null){b.I(p) +return}b.a^=2 +A.aj(null,null,b.b,new A.cG(q,b))}, +ah(a,b){var s,r,q,p,o,n,m,l,k,j,i,h,g={},f=g.a=a +for(;!0;){s={} +r=f.a +q=(r&16)===0 +p=!q +if(b==null){if(p&&(r&1)===0){f=f.c +A.bY(f.a,f.b)}return}s.a=b +o=b.a +for(f=b;o!=null;f=o,o=n){f.a=null +A.ah(g.a,f) +s.a=o +n=o.a}r=g.a +m=r.c +s.b=p +s.c=m +if(q){l=f.c +l=(l&1)!==0||(l&15)===8}else l=!0 +if(l){k=f.b.b +if(p){r=r.b===k +r=!(r||r)}else r=!1 +if(r){A.bY(m.a,m.b) +return}j=$.j +if(j!==k)$.j=k +else j=null +f=f.c +if((f&15)===8)new A.cN(s,g,p).$0() +else if(q){if((f&1)!==0)new A.cM(s,m).$0()}else if((f&2)!==0)new A.cL(g,s).$0() +if(j!=null)$.j=j +f=s.c +if(f instanceof A.o){r=s.a.$ti +r=r.i("a7<2>").b(f)||!r.y[1].b(f)}else r=!1 +if(r){i=s.a.b +if((f.a&24)!==0){h=i.c +i.c=null +b=i.L(h) +i.a=f.a&30|i.a&1 +i.c=f.c +g.a=f +continue}else A.e5(f,i) +return}}i=s.a.b +h=i.c +i.c=null +b=i.L(h) +f=s.b +r=s.c +if(!f){i.a=8 +i.c=r}else{i.a=i.a&1|16 +i.c=r}g.a=i +f=i}}, +ha(a,b){if(t.C.b(a))return b.a4(a) +if(t.v.b(a))return a +throw A.b(A.dN(a,"onError",u.c))}, +h6(){var s,r +for(s=$.ai;s!=null;s=$.ai){$.b8=null +r=s.b +$.ai=r +if(r==null)$.b7=null +s.a.$0()}}, +hd(){$.dx=!0 +try{A.h6()}finally{$.b8=null +$.dx=!1 +if($.ai!=null)$.dL().$1(A.ev())}}, +et(a){var s=new A.bH(a),r=$.b7 +if(r==null){$.ai=$.b7=s +if(!$.dx)$.dL().$1(A.ev())}else $.b7=r.b=s}, +hc(a){var s,r,q,p=$.ai +if(p==null){A.et(a) +$.b8=$.b7 +return}s=new A.bH(a) +r=$.b8 +if(r==null){s.b=p +$.ai=$.b8=s}else{q=r.b +s.b=q +$.b8=r.b=s +if(q==null)$.b7=s}}, +dI(a){var s=null,r=$.j +if(B.a===r){A.aj(s,s,B.a,a) +return}A.aj(s,s,r,r.am(a))}, +hO(a){A.d8(a,"stream",t.K) +return new A.bS()}, +bZ(a){return}, +fk(a,b,c,d,e){var s=$.j,r=e?1:0,q=c!=null?32:0 +A.e3(s,c) +return new A.ae(a,b,s,r|q)}, +e3(a,b){if(b==null)b=A.ho() +if(t.j.b(b))return a.a4(b) +if(t.u.b(b))return b +throw A.b(A.c1("handleError callback must take either an Object (the error), or both an Object (the error) and a StackTrace.",null))}, +h7(a,b){A.bY(a,b)}, +bY(a,b){A.hc(new A.d5(a,b))}, +eq(a,b,c,d){var s,r=$.j +if(r===c)return d.$0() +$.j=c +s=r +try{r=d.$0() +return r}finally{$.j=s}}, +er(a,b,c,d,e){var s,r=$.j +if(r===c)return d.$1(e) +$.j=c +s=r +try{r=d.$1(e) +return r}finally{$.j=s}}, +hb(a,b,c,d,e,f){var s,r=$.j +if(r===c)return d.$2(e,f) +$.j=c +s=r +try{r=d.$2(e,f) +return r}finally{$.j=s}}, +aj(a,b,c,d){if(B.a!==c)d=c.am(d) +A.et(d)}, +cz:function cz(a){this.a=a}, +cy:function cy(a,b,c){this.a=a +this.b=b +this.c=c}, +cA:function cA(a){this.a=a}, +cB:function cB(a){this.a=a}, +cW:function cW(){}, +cX:function cX(a,b){this.a=a +this.b=b}, +bG:function bG(a,b){this.a=a +this.b=!1 +this.$ti=b}, +d1:function d1(a){this.a=a}, +d2:function d2(a){this.a=a}, +d6:function d6(a){this.a=a}, +be:function be(a,b){this.a=a +this.b=b}, +aO:function aO(a,b){this.a=a +this.$ti=b}, +aP:function aP(a,b,c,d){var _=this +_.ay=0 +_.CW=_.ch=null +_.w=a +_.a=b +_.d=c +_.e=d +_.r=null}, +ad:function ad(){}, +b1:function b1(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.e=_.d=null +_.$ti=c}, +cV:function cV(a,b){this.a=a +this.b=b}, +ag:function ag(a,b,c,d,e){var _=this +_.a=null +_.b=a +_.c=b +_.d=c +_.e=d +_.$ti=e}, +o:function o(a,b){var _=this +_.a=0 +_.b=a +_.c=null +_.$ti=b}, +cD:function cD(a,b){this.a=a +this.b=b}, +cK:function cK(a,b){this.a=a +this.b=b}, +cH:function cH(a){this.a=a}, +cI:function cI(a){this.a=a}, +cJ:function cJ(a,b,c){this.a=a +this.b=b +this.c=c}, +cG:function cG(a,b){this.a=a +this.b=b}, +cF:function cF(a,b){this.a=a +this.b=b}, +cE:function cE(a,b,c){this.a=a +this.b=b +this.c=c}, +cN:function cN(a,b,c){this.a=a +this.b=b +this.c=c}, +cO:function cO(a){this.a=a}, +cM:function cM(a,b){this.a=a +this.b=b}, +cL:function cL(a,b){this.a=a +this.b=b}, +bH:function bH(a){this.a=a +this.b=null}, +ab:function ab(){}, +cn:function cn(a,b){this.a=a +this.b=b}, +co:function co(a,b){this.a=a +this.b=b}, +bR:function bR(){}, +cU:function cU(a){this.a=a}, +bI:function bI(){}, +ac:function ac(a,b,c,d){var _=this +_.a=null +_.b=0 +_.d=a +_.e=b +_.f=c +_.$ti=d}, +U:function U(a,b){this.a=a +this.$ti=b}, +ae:function ae(a,b,c,d){var _=this +_.w=a +_.a=b +_.d=c +_.e=d +_.r=null}, +a2:function a2(){}, +b0:function b0(){}, +bK:function bK(){}, +af:function af(a){this.b=a +this.a=null}, +aZ:function aZ(){this.a=0 +this.c=this.b=null}, +cQ:function cQ(a,b){this.a=a +this.b=b}, +aQ:function aQ(a){this.a=1 +this.b=a +this.c=null}, +bS:function bS(){}, +d0:function d0(){}, +d5:function d5(a,b){this.a=a +this.b=b}, +cS:function cS(){}, +cT:function cT(a,b){this.a=a +this.b=b}, +e6(a,b){var s=a[b] +return s===a?null:s}, +e7(a,b,c){if(c==null)a[b]=a +else a[b]=c}, +fm(){var s=Object.create(null) +A.e7(s,"",s) +delete s[""] +return s}, +cg(a){var s,r={} +if(A.dD(a))return"{...}" +s=new A.aK("") +try{$.x.push(a) +s.a+="{" +r.a=!0 +a.t(0,new A.ch(r,s)) +s.a+="}"}finally{if(0>=$.x.length)return A.B($.x,-1) +$.x.pop()}r=s.a +return r.charCodeAt(0)==0?r:r}, +aR:function aR(){}, +aT:function aT(a){var _=this +_.a=0 +_.e=_.d=_.c=_.b=null +_.$ti=a}, +aS:function aS(a,b){this.a=a +this.$ti=b}, +bN:function bN(a,b,c){var _=this +_.a=a +_.b=b +_.c=0 +_.d=null +_.$ti=c}, +i:function i(){}, +y:function y(){}, +ch:function ch(a,b){this.a=a +this.b=b}, +bV:function bV(){}, +aB:function aB(){}, +aN:function aN(){}, +b6:function b6(){}, +h8(a,b){var s,r,q,p=null +try{p=JSON.parse(a)}catch(r){s=A.O(r) +q=String(s) +throw A.b(new A.c7(q))}q=A.d3(p) +return q}, +d3(a){var s +if(a==null)return null +if(typeof a!="object")return a +if(!Array.isArray(a))return new A.bO(a,Object.create(null)) +for(s=0;s4294967295)A.ba(A.fd(a,0,4294967295,"length",null)) +s=J.dV(A.W(new Array(a),c.i("r<0>"))) +if(a!==0&&b!=null)for(r=s.length,q=0;q")) +s=A.W([],b.i("r<0>")) +for(r=J.dk(a);r.l();)s.push(r.gm()) +return s}, +e0(a,b,c){var s=J.dk(b) +if(!s.l())return a +if(c.length===0){do a+=A.n(s.gm()) +while(s.l())}else{a+=A.n(s.gm()) +for(;s.l();)a=a+c+A.n(s.gm())}return a}, +dW(a,b){return new A.bA(a,b.gb_(),b.gb1(),b.gb0())}, +a6(a){if(typeof a=="number"||A.d4(a)||a==null)return J.bb(a) +if(typeof a=="string")return JSON.stringify(a) +return A.fc(a)}, +f1(a,b){A.d8(a,"error",t.K) +A.d8(b,"stackTrace",t.l) +A.f0(a,b)}, +bd(a){return new A.bc(a)}, +c1(a,b){return new A.P(!1,null,b,a)}, +dN(a,b,c){return new A.P(!0,a,b,c)}, +fd(a,b,c,d,e){return new A.aI(b,c,!0,a,d,"Invalid value")}, +dT(a,b,c,d){return new A.bi(b,!0,a,d,"Index out of range")}, +ds(a){return new A.bF(a)}, +e2(a){return new A.bD(a)}, +e_(a){return new A.H(a)}, +a0(a){return new A.bg(a)}, +f2(a,b,c){var s,r +if(A.dD(a)){if(b==="("&&c===")")return"(...)" +return b+"..."+c}s=A.W([],t.s) +$.x.push(a) +try{A.h4(a,s)}finally{if(0>=$.x.length)return A.B($.x,-1) +$.x.pop()}r=A.e0(b,s,", ")+c +return r.charCodeAt(0)==0?r:r}, +dU(a,b,c){var s,r +if(A.dD(a))return b+"..."+c +s=new A.aK(b) +$.x.push(a) +try{r=s +r.a=A.e0(r.a,a,", ")}finally{if(0>=$.x.length)return A.B($.x,-1) +$.x.pop()}s.a+=c +r=s.a +return r.charCodeAt(0)==0?r:r}, +h4(a,b){var s,r,q,p,o,n,m,l=a.gq(a),k=0,j=0 +while(!0){if(!(k<80||j<3))break +if(!l.l())return +s=A.n(l.gm()) +b.push(s) +k+=s.length+2;++j}if(!l.l()){if(j<=5)return +if(0>=b.length)return A.B(b,-1) +r=b.pop() +if(0>=b.length)return A.B(b,-1) +q=b.pop()}else{p=l.gm();++j +if(!l.l()){if(j<=4){b.push(A.n(p)) +return}r=A.n(p) +if(0>=b.length)return A.B(b,-1) +q=b.pop() +k+=r.length+2}else{o=l.gm();++j +for(;l.l();p=o,o=n){n=l.gm();++j +if(j>100){while(!0){if(!(k>75&&j>3))break +if(0>=b.length)return A.B(b,-1) +k-=b.pop().length+2;--j}b.push("...") +return}}q=A.n(p) +r=A.n(o) +k+=r.length+q.length+4}}if(j>b.length+2){k+=5 +m="..."}else m=null +while(!0){if(!(k>80&&b.length>3))break +if(0>=b.length)return A.B(b,-1) +k-=b.pop().length+2 +if(m==null){k+=5 +m="..."}}if(m!=null)b.push(m) +b.push(q) +b.push(r)}, +dH(a){A.hH(a)}, +ci:function ci(a,b){this.a=a +this.b=b}, +h:function h(){}, +bc:function bc(a){this.a=a}, +I:function I(){}, +P:function P(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +aI:function aI(a,b,c,d,e,f){var _=this +_.e=a +_.f=b +_.a=c +_.b=d +_.c=e +_.d=f}, +bi:function bi(a,b,c,d,e){var _=this +_.f=a +_.a=b +_.b=c +_.c=d +_.d=e}, +bA:function bA(a,b,c,d){var _=this +_.a=a +_.b=b +_.c=c +_.d=d}, +bF:function bF(a){this.a=a}, +bD:function bD(a){this.a=a}, +H:function H(a){this.a=a}, +bg:function bg(a){this.a=a}, +aJ:function aJ(){}, +cC:function cC(a){this.a=a}, +c7:function c7(a){this.a=a}, +c:function c(){}, +p:function p(){}, +d:function d(){}, +bT:function bT(){}, +aK:function aK(a){this.a=a}, +fN(a){var s,r=a.$dart_jsFunction +if(r!=null)return r +s=function(b,c){return function(){return b(c,Array.prototype.slice.apply(arguments))}}(A.fM,a) +s[$.dK()]=a +a.$dart_jsFunction=s +return s}, +fM(a,b){return A.fa(a,b,null)}, +hk(a){if(typeof a=="function")return a +else return A.fN(a)}, +ep(a){return a==null||A.d4(a)||typeof a=="number"||typeof a=="string"||t.U.b(a)||t.G.b(a)||t.e.b(a)||t.O.b(a)||t.E.b(a)||t.k.b(a)||t.w.b(a)||t.D.b(a)||t.q.b(a)||t.J.b(a)||t.Y.b(a)}, +hD(a){if(A.ep(a))return a +return new A.dg(new A.aT(t.M)).$1(a)}, +dg:function dg(a){this.a=a}, +hq(a,b,c,d,e){var s=e.i("b1<0>"),r=new A.b1(null,null,s) +a[b]=t.g.a(A.hk(new A.d7(r,c,d))) +return new A.aO(r,s.i("aO<1>"))}, +ff(){var s=new A.cw() +s.az() +return s}, +dE(){var s=0,r=A.h5(t.n),q,p +var $async$dE=A.hj(function(a,b){if(a===1)return A.fI(b,r) +while(true)switch(s){case 0:A.dH("Worker created.\n") +q=A.ff() +p=q.a +p===$&&A.eC() +new A.U(p,A.D(p).i("U<1>")).aY(new A.dh(q)) +return A.fJ(null,r)}}) +return A.fK($async$dE,r)}, +d7:function d7(a,b,c){this.a=a +this.b=b +this.c=c}, +cw:function cw(){this.a=$}, +cx:function cx(a){this.a=a}, +dh:function dh(a){this.a=a}, +hH(a){if(typeof dartPrint=="function"){dartPrint(a) +return}if(typeof console=="object"&&typeof console.log!="undefined"){console.log(a) +return}if(typeof print=="function"){print(a) +return}throw"Unable to print message: "+String(a)}, +hJ(a){A.eB(new A.aA("Field '"+a+"' has been assigned during initialization."),new Error())}, +eC(){A.eB(new A.aA("Field '' has not been initialized."),new Error())}, +f3(a,b,c,d,e,f){var s +if(c==null)return a[b]() +else{s=a[b](c) +return s}}},B={} +var w=[A,J,B] +var $={} +A.dn.prototype={} +J.bj.prototype={ +C(a,b){return a===b}, +gp(a){return A.aH(a)}, +h(a){return"Instance of '"+A.cl(a)+"'"}, +aq(a,b){throw A.b(A.dW(a,b))}, +gn(a){return A.a4(A.dw(this))}} +J.bk.prototype={ +h(a){return String(a)}, +gp(a){return a?519018:218159}, +gn(a){return A.a4(t.y)}, +$if:1} +J.av.prototype={ +C(a,b){return null==b}, +h(a){return"null"}, +gp(a){return 0}, +$if:1, +$ip:1} +J.ax.prototype={$im:1} +J.R.prototype={ +gp(a){return 0}, +h(a){return String(a)}} +J.bB.prototype={} +J.aM.prototype={} +J.Q.prototype={ +h(a){var s=a[$.dK()] +if(s==null)return this.av(a) +return"JavaScript function for "+J.bb(s)}} +J.aw.prototype={ +gp(a){return 0}, +h(a){return String(a)}} +J.ay.prototype={ +gp(a){return 0}, +h(a){return String(a)}} +J.r.prototype={ +F(a,b){if(!!a.fixed$length)A.ba(A.ds("add")) +a.push(b)}, +a2(a,b){var s +if(!!a.fixed$length)A.ba(A.ds("addAll")) +if(Array.isArray(b)){this.aB(a,b) +return}for(s=J.dk(b);s.l();)a.push(s.gm())}, +aB(a,b){var s,r=b.length +if(r===0)return +if(a===b)throw A.b(A.a0(a)) +for(s=0;s").A(c).i("G<1,2>"))}, +B(a,b){if(!(b"))}, +gp(a){return A.aH(a)}, +gj(a){return a.length}, +k(a,b){if(!(b>=0&&b=p){r.d=null +return!1}r.d=q[s] +r.c=s+1 +return!0}} +J.bm.prototype={ +h(a){if(a===0&&1/a<0)return"-0.0" +else return""+a}, +gp(a){var s,r,q,p,o=a|0 +if(a===o)return o&536870911 +s=Math.abs(a) +r=Math.log(s)/0.6931471805599453|0 +q=Math.pow(2,r) +p=s<1?s/q:q/s +return((p*9007199254740992|0)+(p*3542243181176521|0))*599197+r*1259&536870911}, +aS(a,b){var s +if(a>0)s=this.aR(a,b) +else{s=b>31?31:b +s=a>>s>>>0}return s}, +aR(a,b){return b>31?0:a>>>b}, +gn(a){return A.a4(t.H)}, +$il:1} +J.au.prototype={ +gn(a){return A.a4(t.S)}, +$if:1, +$ia:1} +J.bl.prototype={ +gn(a){return A.a4(t.i)}, +$if:1} +J.a8.prototype={ +au(a,b){return a+b}, +h(a){return a}, +gp(a){var s,r,q +for(s=a.length,r=0,q=0;q>6}r=r+((r&67108863)<<3)&536870911 +r^=r>>11 +return r+((r&16383)<<15)&536870911}, +gn(a){return A.a4(t.N)}, +gj(a){return a.length}, +$if:1, +$iq:1} +A.aA.prototype={ +h(a){return"LateInitializationError: "+this.a}} +A.e.prototype={} +A.C.prototype={ +gq(a){var s=this +return new A.a9(s,s.gj(s),A.D(s).i("a9"))}, +M(a,b,c){return new A.G(this,b,A.D(this).i("@").A(c).i("G<1,2>"))}} +A.a9.prototype={ +gm(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +l(){var s,r=this,q=r.a,p=J.c_(q),o=p.gj(q) +if(r.b!==o)throw A.b(A.a0(q)) +s=r.c +if(s>=o){r.d=null +return!1}r.d=p.B(q,s);++r.c +return!0}} +A.a1.prototype={ +gq(a){var s=this.a,r=A.D(this) +return new A.bp(s.gq(s),this.b,r.i("@<1>").A(r.y[1]).i("bp<1,2>"))}, +gj(a){var s=this.a +return s.gj(s)}} +A.ar.prototype={$ie:1} +A.bp.prototype={ +l(){var s=this,r=s.b +if(r.l()){s.a=s.c.$1(r.gm()) +return!0}s.a=null +return!1}, +gm(){var s=this.a +return s==null?this.$ti.y[1].a(s):s}} +A.G.prototype={ +gj(a){return J.c0(this.a)}, +B(a,b){return this.b.$1(J.eP(this.a,b))}} +A.at.prototype={} +A.T.prototype={ +gp(a){var s=this._hashCode +if(s!=null)return s +s=664597*B.h.gp(this.a)&536870911 +this._hashCode=s +return s}, +h(a){return'Symbol("'+this.a+'")'}, +C(a,b){if(b==null)return!1 +return b instanceof A.T&&this.a===b.a}, +$iaL:1} +A.ap.prototype={} +A.ao.prototype={ +h(a){return A.cg(this)}, +$iu:1} +A.aq.prototype={ +gj(a){return this.b.length}, +gag(){var s=this.$keys +if(s==null){s=Object.keys(this.a) +this.$keys=s}return s}, +G(a){if(typeof a!="string")return!1 +if("__proto__"===a)return!1 +return this.a.hasOwnProperty(a)}, +k(a,b){if(!this.G(b))return null +return this.b[this.a[b]]}, +t(a,b){var s,r,q=this.gag(),p=this.b +for(s=q.length,r=0;r"))}} +A.aU.prototype={ +gj(a){return this.a.length}, +gq(a){var s=this.a +return new A.bQ(s,s.length,this.$ti.i("bQ<1>"))}} +A.bQ.prototype={ +gm(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +l(){var s=this,r=s.c +if(r>=s.b){s.d=null +return!1}s.d=s.a[r] +s.c=r+1 +return!0}} +A.cb.prototype={ +gb_(){var s=this.a +if(s instanceof A.T)return s +return this.a=new A.T(s)}, +gb1(){var s,r,q,p,o,n=this +if(n.c===1)return B.i +s=n.d +r=J.c_(s) +q=r.gj(s)-J.c0(n.e)-n.f +if(q===0)return B.i +p=[] +for(o=0;o>>0}, +h(a){return"Closure '"+this.$_name+"' of "+("Instance of '"+A.cl(this.a)+"'")}} +A.bJ.prototype={ +h(a){return"Reading static variable '"+this.a+"' during its initialization"}} +A.bC.prototype={ +h(a){return"RuntimeError: "+this.a}} +A.cR.prototype={} +A.az.prototype={ +gj(a){return this.a}, +gv(){return new A.F(this,A.D(this).i("F<1>"))}, +G(a){var s=this.b +if(s==null)return!1 +return s[a]!=null}, +k(a,b){var s,r,q,p,o=null +if(typeof b=="string"){s=this.b +if(s==null)return o +r=s[b] +q=r==null?o:r.b +return q}else if(typeof b=="number"&&(b&0x3fffffff)===b){p=this.c +if(p==null)return o +r=p[b] +q=r==null?o:r.b +return q}else return this.aX(b)}, +aX(a){var s,r,q=this.d +if(q==null)return null +s=q[this.an(a)] +r=this.ao(s,a) +if(r<0)return null +return s[r].b}, +H(a,b,c){var s,r,q,p,o,n,m=this +if(typeof b=="string"){s=m.b +m.a8(s==null?m.b=m.X():s,b,c)}else if(typeof b=="number"&&(b&0x3fffffff)===b){r=m.c +m.a8(r==null?m.c=m.X():r,b,c)}else{q=m.d +if(q==null)q=m.d=m.X() +p=m.an(b) +o=q[p] +if(o==null)q[p]=[m.Y(b,c)] +else{n=m.ao(o,b) +if(n>=0)o[n].b=c +else o.push(m.Y(b,c))}}}, +t(a,b){var s=this,r=s.e,q=s.r +for(;r!=null;){b.$2(r.a,r.b) +if(q!==s.r)throw A.b(A.a0(s)) +r=r.c}}, +a8(a,b,c){var s=a[b] +if(s==null)a[b]=this.Y(b,c) +else s.b=c}, +Y(a,b){var s=this,r=new A.cf(a,b) +if(s.e==null)s.e=s.f=r +else s.f=s.f.c=r;++s.a +s.r=s.r+1&1073741823 +return r}, +an(a){return J.dj(a)&1073741823}, +ao(a,b){var s,r +if(a==null)return-1 +s=a.length +for(r=0;r"]=s +delete s[""] +return s}} +A.cf.prototype={} +A.F.prototype={ +gj(a){return this.a.a}, +gq(a){var s=this.a,r=new A.bo(s,s.r) +r.c=s.e +return r}} +A.bo.prototype={ +gm(){return this.d}, +l(){var s,r=this,q=r.a +if(r.b!==q.r)throw A.b(A.a0(q)) +s=r.c +if(s==null){r.d=null +return!1}else{r.d=s.a +r.c=s.c +return!0}}} +A.dc.prototype={ +$1(a){return this.a(a)}, +$S:7} +A.dd.prototype={ +$2(a,b){return this.a(a,b)}, +$S:8} +A.de.prototype={ +$1(a){return this.a(a)}, +$S:9} +A.bq.prototype={ +gn(a){return B.B}, +$if:1, +$idl:1} +A.aE.prototype={} +A.br.prototype={ +gn(a){return B.C}, +$if:1, +$idm:1} +A.aa.prototype={ +gj(a){return a.length}, +$iw:1} +A.aC.prototype={ +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$ie:1, +$ic:1} +A.aD.prototype={$ie:1,$ic:1} +A.bs.prototype={ +gn(a){return B.D}, +$if:1, +$ic5:1} +A.bt.prototype={ +gn(a){return B.E}, +$if:1, +$ic6:1} +A.bu.prototype={ +gn(a){return B.F}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$ic8:1} +A.bv.prototype={ +gn(a){return B.G}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$ic9:1} +A.bw.prototype={ +gn(a){return B.H}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$ica:1} +A.bx.prototype={ +gn(a){return B.I}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$ics:1} +A.by.prototype={ +gn(a){return B.J}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$ict:1} +A.aF.prototype={ +gn(a){return B.K}, +gj(a){return a.length}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$icu:1} +A.bz.prototype={ +gn(a){return B.L}, +gj(a){return a.length}, +k(a,b){A.a3(b,a,a.length) +return a[b]}, +$if:1, +$icv:1} +A.aV.prototype={} +A.aW.prototype={} +A.aX.prototype={} +A.aY.prototype={} +A.z.prototype={ +i(a){return A.cZ(v.typeUniverse,this,a)}, +A(a){return A.fD(v.typeUniverse,this,a)}} +A.bM.prototype={} +A.cY.prototype={ +h(a){return A.v(this.a,null)}} +A.bL.prototype={ +h(a){return this.a}} +A.b2.prototype={$iI:1} +A.cz.prototype={ +$1(a){var s=this.a,r=s.a +s.a=null +r.$0()}, +$S:2} +A.cy.prototype={ +$1(a){var s,r +this.a.a=a +s=this.b +r=this.c +s.firstChild?s.removeChild(r):s.appendChild(r)}, +$S:10} +A.cA.prototype={ +$0(){this.a.$0()}, +$S:3} +A.cB.prototype={ +$0(){this.a.$0()}, +$S:3} +A.cW.prototype={ +aA(a,b){if(self.setTimeout!=null)self.setTimeout(A.d9(new A.cX(this,b),0),a) +else throw A.b(A.ds("`setTimeout()` not found."))}} +A.cX.prototype={ +$0(){this.b.$0()}, +$S:0} +A.bG.prototype={} +A.d1.prototype={ +$1(a){return this.a.$2(0,a)}, +$S:4} +A.d2.prototype={ +$2(a,b){this.a.$2(1,new A.as(a,b))}, +$S:11} +A.d6.prototype={ +$2(a,b){this.a(a,b)}, +$S:12} +A.be.prototype={ +h(a){return A.n(this.a)}, +$ih:1, +gO(){return this.b}} +A.aO.prototype={} +A.aP.prototype={ +Z(){}, +a_(){}} +A.ad.prototype={ +gW(){return this.c<4}, +ak(a,b,c,d){var s,r,q,p,o,n=this +if((n.c&4)!==0){s=new A.aQ($.j) +A.dI(s.gaL()) +if(c!=null)s.c=c +return s}s=$.j +r=d?1:0 +q=b!=null?32:0 +A.e3(s,b) +p=new A.aP(n,a,s,r|q) +p.CW=p +p.ch=p +p.ay=n.c&1 +o=n.e +n.e=p +p.ch=null +p.CW=o +if(o==null)n.d=p +else o.ch=p +if(n.d===p)A.bZ(n.a) +return p}, +ah(a){}, +ai(a){}, +P(){if((this.c&4)!==0)return new A.H("Cannot add new events after calling close") +return new A.H("Cannot add new events while doing an addStream")}, +aJ(a){var s,r,q,p,o=this,n=o.c +if((n&2)!==0)throw A.b(A.e_(u.g)) +s=o.d +if(s==null)return +r=n&1 +o.c=n^3 +for(;s!=null;){n=s.ay +if((n&1)===r){s.ay=n|2 +a.$1(s) +n=s.ay^=1 +q=s.ch +if((n&4)!==0){p=s.CW +if(p==null)o.d=q +else p.ch=q +if(q==null)o.e=p +else q.CW=p +s.CW=s +s.ch=s}s.ay=n&4294967293 +s=q}else s=s.ch}o.c&=4294967293 +if(o.d==null)o.ab()}, +ab(){if((this.c&4)!==0)if(null.gbd())null.aa(null) +A.bZ(this.b)}} +A.b1.prototype={ +gW(){return A.ad.prototype.gW.call(this)&&(this.c&2)===0}, +P(){if((this.c&2)!==0)return new A.H(u.g) +return this.aw()}, +E(a){var s=this,r=s.d +if(r==null)return +if(r===s.e){s.c|=2 +r.a7(a) +s.c&=4294967293 +if(s.d==null)s.ab() +return}s.aJ(new A.cV(s,a))}} +A.cV.prototype={ +$1(a){a.a7(this.b)}, +$S(){return this.a.$ti.i("~(a2<1>)")}} +A.ag.prototype={ +aZ(a){if((this.c&15)!==6)return!0 +return this.b.b.a5(this.d,a.a)}, +aW(a){var s,r=this.e,q=null,p=a.a,o=this.b.b +if(t.C.b(r))q=o.b6(r,p,a.b) +else q=o.a5(r,p) +try{p=q +return p}catch(s){if(t.d.b(A.O(s))){if((this.c&1)!==0)throw A.b(A.c1("The error handler of Future.then must return a value of the returned future's type","onError")) +throw A.b(A.c1("The error handler of Future.catchError must return a value of the future's type","onError"))}else throw s}}} +A.o.prototype={ +aj(a){this.a=this.a&1|4 +this.c=a}, +a6(a,b,c){var s,r,q=$.j +if(q===B.a){if(b!=null&&!t.C.b(b)&&!t.v.b(b))throw A.b(A.dN(b,"onError",u.c))}else if(b!=null)b=A.ha(b,q) +s=new A.o(q,c.i("o<0>")) +r=b==null?1:3 +this.R(new A.ag(s,r,a,b,this.$ti.i("@<1>").A(c).i("ag<1,2>"))) +return s}, +bb(a,b){return this.a6(a,null,b)}, +al(a,b,c){var s=new A.o($.j,c.i("o<0>")) +this.R(new A.ag(s,19,a,b,this.$ti.i("@<1>").A(c).i("ag<1,2>"))) +return s}, +aP(a){this.a=this.a&1|16 +this.c=a}, +I(a){this.a=a.a&30|this.a&1 +this.c=a.c}, +R(a){var s=this,r=s.a +if(r<=3){a.a=s.c +s.c=a}else{if((r&4)!==0){r=s.c +if((r.a&24)===0){r.R(a) +return}s.I(r)}A.aj(null,null,s.b,new A.cD(s,a))}}, +a0(a){var s,r,q,p,o,n=this,m={} +m.a=a +if(a==null)return +s=n.a +if(s<=3){r=n.c +n.c=a +if(r!=null){q=a.a +for(p=a;q!=null;p=q,q=o)o=q.a +p.a=r}}else{if((s&4)!==0){s=n.c +if((s.a&24)===0){s.a0(a) +return}n.I(s)}m.a=n.L(a) +A.aj(null,null,n.b,new A.cK(m,n))}}, +K(){var s=this.c +this.c=null +return this.L(s)}, +L(a){var s,r,q +for(s=a,r=null;s!=null;r=s,s=q){q=s.a +s.a=r}return r}, +aF(a){var s,r,q,p=this +p.a^=2 +try{a.a6(new A.cH(p),new A.cI(p),t.P)}catch(q){s=A.O(q) +r=A.Y(q) +A.dI(new A.cJ(p,s,r))}}, +S(a){var s=this,r=s.K() +s.a=8 +s.c=a +A.ah(s,r)}, +D(a,b){var s=this.K() +this.aP(A.c2(a,b)) +A.ah(this,s)}, +aa(a){if(this.$ti.i("a7<1>").b(a)){this.ac(a) +return}this.aD(a)}, +aD(a){this.a^=2 +A.aj(null,null,this.b,new A.cF(this,a))}, +ac(a){if(this.$ti.b(a)){A.fl(a,this) +return}this.aF(a)}, +aC(a,b){this.a^=2 +A.aj(null,null,this.b,new A.cE(this,a,b))}, +$ia7:1} +A.cD.prototype={ +$0(){A.ah(this.a,this.b)}, +$S:0} +A.cK.prototype={ +$0(){A.ah(this.b,this.a.a)}, +$S:0} +A.cH.prototype={ +$1(a){var s,r,q,p=this.a +p.a^=2 +try{p.S(p.$ti.c.a(a))}catch(q){s=A.O(q) +r=A.Y(q) +p.D(s,r)}}, +$S:2} +A.cI.prototype={ +$2(a,b){this.a.D(a,b)}, +$S:13} +A.cJ.prototype={ +$0(){this.a.D(this.b,this.c)}, +$S:0} +A.cG.prototype={ +$0(){A.e5(this.a.a,this.b)}, +$S:0} +A.cF.prototype={ +$0(){this.a.S(this.b)}, +$S:0} +A.cE.prototype={ +$0(){this.a.D(this.b,this.c)}, +$S:0} +A.cN.prototype={ +$0(){var s,r,q,p,o,n,m=this,l=null +try{q=m.a.a +l=q.b.b.b4(q.d)}catch(p){s=A.O(p) +r=A.Y(p) +q=m.c&&m.b.a.c.a===s +o=m.a +if(q)o.c=m.b.a.c +else o.c=A.c2(s,r) +o.b=!0 +return}if(l instanceof A.o&&(l.a&24)!==0){if((l.a&16)!==0){q=m.a +q.c=l.c +q.b=!0}return}if(l instanceof A.o){n=m.b.a +q=m.a +q.c=l.bb(new A.cO(n),t.z) +q.b=!1}}, +$S:0} +A.cO.prototype={ +$1(a){return this.a}, +$S:14} +A.cM.prototype={ +$0(){var s,r,q,p,o +try{q=this.a +p=q.a +q.c=p.b.b.a5(p.d,this.b)}catch(o){s=A.O(o) +r=A.Y(o) +q=this.a +q.c=A.c2(s,r) +q.b=!0}}, +$S:0} +A.cL.prototype={ +$0(){var s,r,q,p,o,n,m=this +try{s=m.a.a.c +p=m.b +if(p.a.aZ(s)&&p.a.e!=null){p.c=p.a.aW(s) +p.b=!1}}catch(o){r=A.O(o) +q=A.Y(o) +p=m.a.a.c +n=m.b +if(p.a===r)n.c=p +else n.c=A.c2(r,q) +n.b=!0}}, +$S:0} +A.bH.prototype={} +A.ab.prototype={ +gj(a){var s={},r=new A.o($.j,t.a) +s.a=0 +this.ap(new A.cn(s,this),!0,new A.co(s,r),r.gaG()) +return r}} +A.cn.prototype={ +$1(a){++this.a.a}, +$S(){return A.D(this.b).i("~(1)")}} +A.co.prototype={ +$0(){var s=this.b,r=this.a.a,q=s.K() +s.a=8 +s.c=r +A.ah(s,q)}, +$S:0} +A.bR.prototype={ +gaN(){if((this.b&8)===0)return this.a +return this.a.ga1()}, +aI(){var s,r=this +if((r.b&8)===0){s=r.a +return s==null?r.a=new A.aZ():s}s=r.a.ga1() +return s}, +gaT(){var s=this.a +return(this.b&8)!==0?s.ga1():s}, +aE(){if((this.b&4)!==0)return new A.H("Cannot add event after closing") +return new A.H("Cannot add event while adding a stream")}, +ak(a,b,c,d){var s,r,q,p,o=this +if((o.b&3)!==0)throw A.b(A.e_("Stream has already been listened to.")) +s=A.fk(o,a,b,c,d) +r=o.gaN() +q=o.b|=1 +if((q&8)!==0){p=o.a +p.sa1(s) +p.b3()}else o.a=s +s.aQ(r) +q=s.e +s.e=q|64 +new A.cU(o).$0() +s.e&=4294967231 +s.ad((q&4)!==0) +return s}, +ah(a){if((this.b&8)!==0)this.a.be() +A.bZ(this.e)}, +ai(a){if((this.b&8)!==0)this.a.b3() +A.bZ(this.f)}} +A.cU.prototype={ +$0(){A.bZ(this.a.d)}, +$S:0} +A.bI.prototype={ +E(a){this.gaT().a9(new A.af(a))}} +A.ac.prototype={} +A.U.prototype={ +gp(a){return(A.aH(this.a)^892482866)>>>0}, +C(a,b){if(b==null)return!1 +if(this===b)return!0 +return b instanceof A.U&&b.a===this.a}} +A.ae.prototype={ +Z(){this.w.ah(this)}, +a_(){this.w.ai(this)}} +A.a2.prototype={ +aQ(a){if(a==null)return +this.r=a +if(a.c!=null){this.e|=128 +a.N(this)}}, +a7(a){var s=this.e +if((s&8)!==0)return +if(s<64)this.E(a) +else this.a9(new A.af(a))}, +Z(){}, +a_(){}, +a9(a){var s,r=this,q=r.r +if(q==null)q=r.r=new A.aZ() +q.F(0,a) +s=r.e +if((s&128)===0){s|=128 +r.e=s +if(s<256)q.N(r)}}, +E(a){var s=this,r=s.e +s.e=r|64 +s.d.ba(s.a,a) +s.e&=4294967231 +s.ad((r&4)!==0)}, +ad(a){var s,r,q=this,p=q.e +if((p&128)!==0&&q.r.c==null){p=q.e=p&4294967167 +if((p&4)!==0)if(p<256){s=q.r +s=s==null?null:s.c==null +s=s!==!1}else s=!1 +else s=!1 +if(s){p&=4294967291 +q.e=p}}for(;!0;a=r){if((p&8)!==0){q.r=null +return}r=(p&4)!==0 +if(a===r)break +q.e=p^64 +if(r)q.Z() +else q.a_() +p=q.e&=4294967231}if((p&128)!==0&&p<256)q.r.N(q)}} +A.b0.prototype={ +ap(a,b,c,d){return this.a.ak(a,d,c,b===!0)}, +aY(a){return this.ap(a,null,null,null)}} +A.bK.prototype={} +A.af.prototype={} +A.aZ.prototype={ +N(a){var s=this,r=s.a +if(r===1)return +if(r>=1){s.a=1 +return}A.dI(new A.cQ(s,a)) +s.a=1}, +F(a,b){var s=this,r=s.c +if(r==null)s.b=s.c=b +else s.c=r.a=b}} +A.cQ.prototype={ +$0(){var s,r,q=this.a,p=q.a +q.a=0 +if(p===3)return +s=q.b +r=s.a +q.b=r +if(r==null)q.c=null +this.b.E(s.b)}, +$S:0} +A.aQ.prototype={ +aM(){var s,r=this,q=r.a-1 +if(q===0){r.a=-1 +s=r.c +if(s!=null){r.c=null +r.b.ar(s)}}else r.a=q}} +A.bS.prototype={} +A.d0.prototype={} +A.d5.prototype={ +$0(){A.f1(this.a,this.b)}, +$S:0} +A.cS.prototype={ +ar(a){var s,r,q +try{if(B.a===$.j){a.$0() +return}A.eq(null,null,this,a)}catch(q){s=A.O(q) +r=A.Y(q) +A.bY(s,r)}}, +b9(a,b){var s,r,q +try{if(B.a===$.j){a.$1(b) +return}A.er(null,null,this,a,b)}catch(q){s=A.O(q) +r=A.Y(q) +A.bY(s,r)}}, +ba(a,b){return this.b9(a,b,t.z)}, +am(a){return new A.cT(this,a)}, +b5(a){if($.j===B.a)return a.$0() +return A.eq(null,null,this,a)}, +b4(a){return this.b5(a,t.z)}, +b8(a,b){if($.j===B.a)return a.$1(b) +return A.er(null,null,this,a,b)}, +a5(a,b){var s=t.z +return this.b8(a,b,s,s)}, +b7(a,b,c){if($.j===B.a)return a.$2(b,c) +return A.hb(null,null,this,a,b,c)}, +b6(a,b,c){var s=t.z +return this.b7(a,b,c,s,s,s)}, +b2(a){return a}, +a4(a){var s=t.z +return this.b2(a,s,s,s)}} +A.cT.prototype={ +$0(){return this.a.ar(this.b)}, +$S:0} +A.aR.prototype={ +gj(a){return this.a}, +gv(){return new A.aS(this,this.$ti.i("aS<1>"))}, +G(a){var s,r +if(typeof a=="string"&&a!=="__proto__"){s=this.b +return s==null?!1:s[a]!=null}else if(typeof a=="number"&&(a&1073741823)===a){r=this.c +return r==null?!1:r[a]!=null}else return this.aH(a)}, +aH(a){var s=this.d +if(s==null)return!1 +return this.V(this.af(s,a),a)>=0}, +k(a,b){var s,r,q +if(typeof b=="string"&&b!=="__proto__"){s=this.b +r=s==null?null:A.e6(s,b) +return r}else if(typeof b=="number"&&(b&1073741823)===b){q=this.c +r=q==null?null:A.e6(q,b) +return r}else return this.aK(b)}, +aK(a){var s,r,q=this.d +if(q==null)return null +s=this.af(q,a) +r=this.V(s,a) +return r<0?null:s[r+1]}, +H(a,b,c){var s,r,q,p=this,o=p.d +if(o==null)o=p.d=A.fm() +s=A.dG(b)&1073741823 +r=o[s] +if(r==null){A.e7(o,s,[b,c]);++p.a +p.e=null}else{q=p.V(r,b) +if(q>=0)r[q+1]=c +else{r.push(b,c);++p.a +p.e=null}}}, +t(a,b){var s,r,q,p,o,n=this,m=n.ae() +for(s=m.length,r=n.$ti.y[1],q=0;q"))}} +A.bN.prototype={ +gm(){var s=this.d +return s==null?this.$ti.c.a(s):s}, +l(){var s=this,r=s.b,q=s.c,p=s.a +if(r!==p.e)throw A.b(A.a0(p)) +else if(q>=r.length){s.d=null +return!1}else{s.d=r[q] +s.c=q+1 +return!0}}} +A.i.prototype={ +gq(a){return new A.a9(a,this.gj(a),A.am(a).i("a9"))}, +B(a,b){return this.k(a,b)}, +M(a,b,c){return new A.G(a,b,A.am(a).i("@").A(c).i("G<1,2>"))}, +h(a){return A.dU(a,"[","]")}} +A.y.prototype={ +t(a,b){var s,r,q,p +for(s=this.gv(),s=s.gq(s),r=A.D(this).i("y.V");s.l();){q=s.gm() +p=this.k(0,q) +b.$2(q,p==null?r.a(p):p)}}, +gj(a){var s=this.gv() +return s.gj(s)}, +h(a){return A.cg(this)}, +$iu:1} +A.ch.prototype={ +$2(a,b){var s,r=this.a +if(!r.a)this.b.a+=", " +r.a=!1 +r=this.b +s=A.n(a) +s=r.a+=s +r.a=s+": " +s=A.n(b) +r.a+=s}, +$S:15} +A.bV.prototype={} +A.aB.prototype={ +k(a,b){return this.a.k(0,b)}, +t(a,b){this.a.t(0,b)}, +gj(a){return this.a.a}, +gv(){var s=this.a +return new A.F(s,s.$ti.i("F<1>"))}, +h(a){return A.cg(this.a)}, +$iu:1} +A.aN.prototype={} +A.b6.prototype={} +A.bO.prototype={ +k(a,b){var s,r=this.b +if(r==null)return this.c.k(0,b) +else if(typeof b!="string")return null +else{s=r[b] +return typeof s=="undefined"?this.aO(b):s}}, +gj(a){return this.b==null?this.c.a:this.J().length}, +gv(){if(this.b==null){var s=this.c +return new A.F(s,A.D(s).i("F<1>"))}return new A.bP(this)}, +t(a,b){var s,r,q,p,o=this +if(o.b==null)return o.c.t(0,b) +s=o.J() +for(r=0;r"))}return s}} +A.bf.prototype={} +A.bh.prototype={} +A.cd.prototype={ +aU(a,b){var s=A.h8(a,this.gaV().a) +return s}, +gaV(){return B.y}} +A.ce.prototype={} +A.ci.prototype={ +$2(a,b){var s=this.b,r=this.a,q=s.a+=r.a +q+=a.a +s.a=q +s.a=q+": " +q=A.a6(b) +s.a+=q +r.a=", "}, +$S:16} +A.h.prototype={ +gO(){return A.fb(this)}} +A.bc.prototype={ +h(a){var s=this.a +if(s!=null)return"Assertion failed: "+A.a6(s) +return"Assertion failed"}} +A.I.prototype={} +A.P.prototype={ +gU(){return"Invalid argument"+(!this.a?"(s)":"")}, +gT(){return""}, +h(a){var s=this,r=s.c,q=r==null?"":" ("+r+")",p=s.d,o=p==null?"":": "+p,n=s.gU()+q+o +if(!s.a)return n +return n+s.gT()+": "+A.a6(s.ga3())}, +ga3(){return this.b}} +A.aI.prototype={ +ga3(){return this.b}, +gU(){return"RangeError"}, +gT(){var s,r=this.e,q=this.f +if(r==null)s=q!=null?": Not less than or equal to "+A.n(q):"" +else if(q==null)s=": Not greater than or equal to "+A.n(r) +else if(q>r)s=": Not in inclusive range "+A.n(r)+".."+A.n(q) +else s=q=4)A.ba(q.aE()) +if((s&1)!==0)q.E(r) +else if((s&3)===0)q.aI().F(0,new A.af(r))}, +$S:19} +A.dh.prototype={ +$1(a){var s,r,q,p=null +if(typeof a=="string")try{s=t.f.a(B.r.aU(a,p)) +A.dH("Received "+a+" PARSED TO "+A.n(s)+"\n") +if(J.dM(J.eO(s,"message"),"voiceEndedCallback")){r=t.m.a(self) +A.f3(r,"postMessage",A.hD(a),p,p,p)}}catch(q){A.dH("Received data from WASM worker but it's not a String!\n")}}, +$S:4};(function aliases(){var s=J.R.prototype +s.av=s.h +s=A.ad.prototype +s.aw=s.P})();(function installTearOffs(){var s=hunkHelpers._static_1,r=hunkHelpers._static_0,q=hunkHelpers._static_2,p=hunkHelpers._instance_2u,o=hunkHelpers._instance_0u +s(A,"hl","fh",1) +s(A,"hm","fi",1) +s(A,"hn","fj",1) +r(A,"ev","hd",0) +q(A,"ho","h7",5) +p(A.o.prototype,"gaG","D",5) +o(A.aQ.prototype,"gaL","aM",0)})();(function inheritance(){var s=hunkHelpers.mixin,r=hunkHelpers.inherit,q=hunkHelpers.inheritMany +r(A.d,null) +q(A.d,[A.dn,J.bj,J.a5,A.h,A.c,A.a9,A.bp,A.at,A.T,A.aB,A.ao,A.bQ,A.cb,A.a_,A.cq,A.cj,A.as,A.b_,A.cR,A.y,A.cf,A.bo,A.z,A.bM,A.cY,A.cW,A.bG,A.be,A.ab,A.a2,A.ad,A.ag,A.o,A.bH,A.bR,A.bI,A.bK,A.aZ,A.aQ,A.bS,A.d0,A.bN,A.i,A.bV,A.bf,A.bh,A.aJ,A.cC,A.c7,A.p,A.bT,A.aK,A.cw]) +q(J.bj,[J.bk,J.av,J.ax,J.aw,J.ay,J.bm,J.a8]) +q(J.ax,[J.R,J.r,A.bq,A.aE]) +q(J.R,[J.bB,J.aM,J.Q]) +r(J.cc,J.r) +q(J.bm,[J.au,J.bl]) +q(A.h,[A.aA,A.I,A.bn,A.bE,A.bJ,A.bC,A.bL,A.bc,A.P,A.bA,A.bF,A.bD,A.H,A.bg]) +q(A.c,[A.e,A.a1,A.aU]) +q(A.e,[A.C,A.F,A.aS]) +r(A.ar,A.a1) +q(A.C,[A.G,A.bP]) +r(A.b6,A.aB) +r(A.aN,A.b6) +r(A.ap,A.aN) +r(A.aq,A.ao) +q(A.a_,[A.c4,A.c3,A.cp,A.dc,A.de,A.cz,A.cy,A.d1,A.cV,A.cH,A.cO,A.cn,A.dg,A.d7,A.cx,A.dh]) +q(A.c4,[A.ck,A.dd,A.d2,A.d6,A.cI,A.ch,A.ci]) +r(A.aG,A.I) +q(A.cp,[A.cm,A.an]) +q(A.y,[A.az,A.aR,A.bO]) +q(A.aE,[A.br,A.aa]) +q(A.aa,[A.aV,A.aX]) +r(A.aW,A.aV) +r(A.aC,A.aW) +r(A.aY,A.aX) +r(A.aD,A.aY) +q(A.aC,[A.bs,A.bt]) +q(A.aD,[A.bu,A.bv,A.bw,A.bx,A.by,A.aF,A.bz]) +r(A.b2,A.bL) +q(A.c3,[A.cA,A.cB,A.cX,A.cD,A.cK,A.cJ,A.cG,A.cF,A.cE,A.cN,A.cM,A.cL,A.co,A.cU,A.cQ,A.d5,A.cT]) +r(A.b0,A.ab) +r(A.U,A.b0) +r(A.aO,A.U) +r(A.ae,A.a2) +r(A.aP,A.ae) +r(A.b1,A.ad) +r(A.ac,A.bR) +r(A.af,A.bK) +r(A.cS,A.d0) +r(A.aT,A.aR) +r(A.cd,A.bf) +r(A.ce,A.bh) +q(A.P,[A.aI,A.bi]) +s(A.aV,A.i) +s(A.aW,A.at) +s(A.aX,A.i) +s(A.aY,A.at) +s(A.ac,A.bI) +s(A.b6,A.bV)})() +var v={typeUniverse:{eC:new Map(),tR:{},eT:{},tPV:{},sEA:[]},mangledGlobalNames:{a:"int",l:"double",hG:"num",q:"String",hp:"bool",p:"Null",f4:"List",d:"Object",u:"Map"},mangledNames:{},types:["~()","~(~())","p(@)","p()","~(@)","~(d,A)","~(q,@)","@(@)","@(@,q)","@(q)","p(~())","p(@,A)","~(a,@)","p(d,A)","o<@>(@)","~(d?,d?)","~(aL,@)","d?(d?)","~(d)","p(m)"],interceptorsByTag:null,leafTags:null,arrayRti:Symbol("$ti")} +A.fC(v.typeUniverse,JSON.parse('{"bB":"R","aM":"R","Q":"R","bk":{"f":[]},"av":{"p":[],"f":[]},"ax":{"m":[]},"R":{"m":[]},"r":{"e":["1"],"m":[],"c":["1"]},"cc":{"r":["1"],"e":["1"],"m":[],"c":["1"]},"bm":{"l":[]},"au":{"l":[],"a":[],"f":[]},"bl":{"l":[],"f":[]},"a8":{"q":[],"f":[]},"aA":{"h":[]},"e":{"c":["1"]},"C":{"e":["1"],"c":["1"]},"a1":{"c":["2"],"c.E":"2"},"ar":{"a1":["1","2"],"e":["2"],"c":["2"],"c.E":"2"},"G":{"C":["2"],"e":["2"],"c":["2"],"c.E":"2","C.E":"2"},"T":{"aL":[]},"ap":{"u":["1","2"]},"ao":{"u":["1","2"]},"aq":{"u":["1","2"]},"aU":{"c":["1"],"c.E":"1"},"aG":{"I":[],"h":[]},"bn":{"h":[]},"bE":{"h":[]},"b_":{"A":[]},"bJ":{"h":[]},"bC":{"h":[]},"az":{"y":["1","2"],"u":["1","2"],"y.V":"2"},"F":{"e":["1"],"c":["1"],"c.E":"1"},"bq":{"m":[],"dl":[],"f":[]},"aE":{"m":[]},"br":{"dm":[],"m":[],"f":[]},"aa":{"w":["1"],"m":[]},"aC":{"i":["l"],"w":["l"],"e":["l"],"m":[],"c":["l"]},"aD":{"i":["a"],"w":["a"],"e":["a"],"m":[],"c":["a"]},"bs":{"i":["l"],"c5":[],"w":["l"],"e":["l"],"m":[],"c":["l"],"f":[],"i.E":"l"},"bt":{"i":["l"],"c6":[],"w":["l"],"e":["l"],"m":[],"c":["l"],"f":[],"i.E":"l"},"bu":{"i":["a"],"c8":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"bv":{"i":["a"],"c9":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"bw":{"i":["a"],"ca":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"bx":{"i":["a"],"cs":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"by":{"i":["a"],"ct":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"aF":{"i":["a"],"cu":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"bz":{"i":["a"],"cv":[],"w":["a"],"e":["a"],"m":[],"c":["a"],"f":[],"i.E":"a"},"bL":{"h":[]},"b2":{"I":[],"h":[]},"o":{"a7":["1"]},"be":{"h":[]},"aO":{"U":["1"],"ab":["1"]},"aP":{"a2":["1"]},"b1":{"ad":["1"]},"ac":{"bR":["1"]},"U":{"ab":["1"]},"ae":{"a2":["1"]},"b0":{"ab":["1"]},"aR":{"y":["1","2"],"u":["1","2"]},"aT":{"aR":["1","2"],"y":["1","2"],"u":["1","2"],"y.V":"2"},"aS":{"e":["1"],"c":["1"],"c.E":"1"},"y":{"u":["1","2"]},"aB":{"u":["1","2"]},"aN":{"u":["1","2"]},"bO":{"y":["q","@"],"u":["q","@"],"y.V":"@"},"bP":{"C":["q"],"e":["q"],"c":["q"],"c.E":"q","C.E":"q"},"bc":{"h":[]},"I":{"h":[]},"P":{"h":[]},"aI":{"h":[]},"bi":{"h":[]},"bA":{"h":[]},"bF":{"h":[]},"bD":{"h":[]},"H":{"h":[]},"bg":{"h":[]},"aJ":{"h":[]},"bT":{"A":[]},"ca":{"e":["a"],"c":["a"]},"cv":{"e":["a"],"c":["a"]},"cu":{"e":["a"],"c":["a"]},"c8":{"e":["a"],"c":["a"]},"cs":{"e":["a"],"c":["a"]},"c9":{"e":["a"],"c":["a"]},"ct":{"e":["a"],"c":["a"]},"c5":{"e":["l"],"c":["l"]},"c6":{"e":["l"],"c":["l"]}}')) +A.fB(v.typeUniverse,JSON.parse('{"e":1,"at":1,"ao":2,"bo":1,"aa":1,"a2":1,"aP":1,"bI":1,"ae":1,"b0":1,"bK":1,"af":1,"aZ":1,"aQ":1,"bS":1,"bV":2,"aB":2,"aN":2,"b6":2,"bf":2,"bh":2}')) +var u={g:"Cannot fire new event. Controller is already firing an event",c:"Error handler must accept one Object or one Object and a StackTrace as arguments, and return a value of the returned future's type"} +var t=(function rtii(){var s=A.dA +return{J:s("dl"),Y:s("dm"),Z:s("ap"),V:s("e<@>"),Q:s("h"),D:s("c5"),q:s("c6"),c:s("hM"),O:s("c8"),k:s("c9"),U:s("ca"),x:s("c"),s:s("r"),b:s("r<@>"),T:s("av"),m:s("m"),g:s("Q"),p:s("w<@>"),B:s("az"),f:s("u<@,@>"),F:s("u"),P:s("p"),K:s("d"),L:s("hN"),l:s("A"),N:s("q"),R:s("f"),d:s("I"),E:s("cs"),w:s("ct"),e:s("cu"),G:s("cv"),o:s("aM"),I:s("ac<@>"),h:s("o<@>"),a:s("o"),M:s("aT"),y:s("hp"),i:s("l"),z:s("@"),v:s("@(d)"),C:s("@(d,A)"),S:s("a"),A:s("0&*"),_:s("d*"),W:s("a7

?"),X:s("d?"),H:s("hG"),n:s("~"),u:s("~(d)"),j:s("~(d,A)")}})();(function constants(){var s=hunkHelpers.makeConstList +B.u=J.bj.prototype +B.b=J.r.prototype +B.v=J.au.prototype +B.h=J.a8.prototype +B.w=J.Q.prototype +B.x=J.ax.prototype +B.k=J.bB.prototype +B.c=J.aM.prototype +B.d=function getTagFallback(o) { + var s = Object.prototype.toString.call(o); + return s.substring(8, s.length - 1); +} +B.l=function() { + var toStringFunction = Object.prototype.toString; + function getTag(o) { + var s = toStringFunction.call(o); + return s.substring(8, s.length - 1); + } + function getUnknownTag(object, tag) { + if (/^HTML[A-Z].*Element$/.test(tag)) { + var name = toStringFunction.call(object); + if (name == "[object Object]") return null; + return "HTMLElement"; + } + } + function getUnknownTagGenericBrowser(object, tag) { + if (object instanceof HTMLElement) return "HTMLElement"; + return getUnknownTag(object, tag); + } + function prototypeForTag(tag) { + if (typeof window == "undefined") return null; + if (typeof window[tag] == "undefined") return null; + var constructor = window[tag]; + if (typeof constructor != "function") return null; + return constructor.prototype; + } + function discriminator(tag) { return null; } + var isBrowser = typeof HTMLElement == "function"; + return { + getTag: getTag, + getUnknownTag: isBrowser ? getUnknownTagGenericBrowser : getUnknownTag, + prototypeForTag: prototypeForTag, + discriminator: discriminator }; +} +B.q=function(getTagFallback) { + return function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("DumpRenderTree") >= 0) return hooks; + if (userAgent.indexOf("Chrome") >= 0) { + function confirm(p) { + return typeof window == "object" && window[p] && window[p].name == p; + } + if (confirm("Window") && confirm("HTMLElement")) return hooks; + } + hooks.getTag = getTagFallback; + }; +} +B.m=function(hooks) { + if (typeof dartExperimentalFixupGetTag != "function") return hooks; + hooks.getTag = dartExperimentalFixupGetTag(hooks.getTag); +} +B.p=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Firefox") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "GeoGeolocation": "Geolocation", + "Location": "!Location", + "WorkerMessageEvent": "MessageEvent", + "XMLDocument": "!Document"}; + function getTagFirefox(o) { + var tag = getTag(o); + return quickMap[tag] || tag; + } + hooks.getTag = getTagFirefox; +} +B.o=function(hooks) { + if (typeof navigator != "object") return hooks; + var userAgent = navigator.userAgent; + if (typeof userAgent != "string") return hooks; + if (userAgent.indexOf("Trident/") == -1) return hooks; + var getTag = hooks.getTag; + var quickMap = { + "BeforeUnloadEvent": "Event", + "DataTransfer": "Clipboard", + "HTMLDDElement": "HTMLElement", + "HTMLDTElement": "HTMLElement", + "HTMLPhraseElement": "HTMLElement", + "Position": "Geoposition" + }; + function getTagIE(o) { + var tag = getTag(o); + var newTag = quickMap[tag]; + if (newTag) return newTag; + if (tag == "Object") { + if (window.DataView && (o instanceof window.DataView)) return "DataView"; + } + return tag; + } + function prototypeForTagIE(tag) { + var constructor = window[tag]; + if (constructor == null) return null; + return constructor.prototype; + } + hooks.getTag = getTagIE; + hooks.prototypeForTag = prototypeForTagIE; +} +B.n=function(hooks) { + var getTag = hooks.getTag; + var prototypeForTag = hooks.prototypeForTag; + function getTagFixed(o) { + var tag = getTag(o); + if (tag == "Document") { + if (!!o.xmlVersion) return "!Document"; + return "!HTMLDocument"; + } + return tag; + } + function prototypeForTagFixed(tag) { + if (tag == "Document") return null; + return prototypeForTag(tag); + } + hooks.getTag = getTagFixed; + hooks.prototypeForTag = prototypeForTagFixed; +} +B.e=function(hooks) { return hooks; } + +B.r=new A.cd() +B.f=new A.cR() +B.a=new A.cS() +B.t=new A.bT() +B.y=new A.ce(null) +B.i=A.W(s([]),t.b) +B.z={} +B.j=new A.aq(B.z,[],A.dA("aq")) +B.A=new A.T("call") +B.B=A.E("dl") +B.C=A.E("dm") +B.D=A.E("c5") +B.E=A.E("c6") +B.F=A.E("c8") +B.G=A.E("c9") +B.H=A.E("ca") +B.I=A.E("cs") +B.J=A.E("ct") +B.K=A.E("cu") +B.L=A.E("cv")})();(function staticFields(){$.cP=null +$.x=A.W([],A.dA("r")) +$.dX=null +$.dQ=null +$.dP=null +$.ex=null +$.eu=null +$.eA=null +$.da=null +$.df=null +$.dC=null +$.ai=null +$.b7=null +$.b8=null +$.dx=!1 +$.j=B.a})();(function lazyInitializers(){var s=hunkHelpers.lazyFinal +s($,"hL","dK",()=>A.ht("_$dart_dartClosure")) +s($,"hP","eE",()=>A.J(A.cr({ +toString:function(){return"$receiver$"}}))) +s($,"hQ","eF",()=>A.J(A.cr({$method$:null, +toString:function(){return"$receiver$"}}))) +s($,"hR","eG",()=>A.J(A.cr(null))) +s($,"hS","eH",()=>A.J(function(){var $argumentsExpr$="$arguments$" +try{null.$method$($argumentsExpr$)}catch(r){return r.message}}())) +s($,"hV","eK",()=>A.J(A.cr(void 0))) +s($,"hW","eL",()=>A.J(function(){var $argumentsExpr$="$arguments$" +try{(void 0).$method$($argumentsExpr$)}catch(r){return r.message}}())) +s($,"hU","eJ",()=>A.J(A.e1(null))) +s($,"hT","eI",()=>A.J(function(){try{null.$method$}catch(r){return r.message}}())) +s($,"hY","eN",()=>A.J(A.e1(void 0))) +s($,"hX","eM",()=>A.J(function(){try{(void 0).$method$}catch(r){return r.message}}())) +s($,"hZ","dL",()=>A.fg())})();(function nativeSupport(){!function(){var s=function(a){var m={} +m[a]=1 +return Object.keys(hunkHelpers.convertToFastObject(m))[0]} +v.getIsolateTag=function(a){return s("___dart_"+a+v.isolateTag)} +var r="___dart_isolate_tags_" +var q=Object[r]||(Object[r]=Object.create(null)) +var p="_ZxYxX" +for(var o=0;;o++){var n=s(p+"_"+o+"_") +if(!(n in q)){q[n]=1 +v.isolateTag=n +break}}v.dispatchPropertyName=v.getIsolateTag("dispatch_record")}() +hunkHelpers.setOrUpdateInterceptorsByTag({ArrayBuffer:A.bq,ArrayBufferView:A.aE,DataView:A.br,Float32Array:A.bs,Float64Array:A.bt,Int16Array:A.bu,Int32Array:A.bv,Int8Array:A.bw,Uint16Array:A.bx,Uint32Array:A.by,Uint8ClampedArray:A.aF,CanvasPixelArray:A.aF,Uint8Array:A.bz}) +hunkHelpers.setOrUpdateLeafTags({ArrayBuffer:true,ArrayBufferView:false,DataView:true,Float32Array:true,Float64Array:true,Int16Array:true,Int32Array:true,Int8Array:true,Uint16Array:true,Uint32Array:true,Uint8ClampedArray:true,CanvasPixelArray:true,Uint8Array:false}) +A.aa.$nativeSuperclassTag="ArrayBufferView" +A.aV.$nativeSuperclassTag="ArrayBufferView" +A.aW.$nativeSuperclassTag="ArrayBufferView" +A.aC.$nativeSuperclassTag="ArrayBufferView" +A.aX.$nativeSuperclassTag="ArrayBufferView" +A.aY.$nativeSuperclassTag="ArrayBufferView" +A.aD.$nativeSuperclassTag="ArrayBufferView"})() +Function.prototype.$1=function(a){return this(a)} +Function.prototype.$0=function(){return this()} +Function.prototype.$2=function(a,b){return this(a,b)} +Function.prototype.$3=function(a,b,c){return this(a,b,c)} +Function.prototype.$4=function(a,b,c,d){return this(a,b,c,d)} +Function.prototype.$1$1=function(a){return this(a)} +convertAllToFastObject(w) +convertToFastObject($);(function(a){if(typeof document==="undefined"){a(null) +return}if(typeof document.currentScript!="undefined"){a(document.currentScript) +return}var s=document.scripts +function onLoad(b){for(var q=0;q