From 5ef9befa5d52547f1dddb936c219ca41c2be78f2 Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Wed, 3 Apr 2024 20:30:07 +0200 Subject: [PATCH 1/3] Improve documentation --- CHANGELOG.md | 9 + LICENSE | 221 +---------- README.md | 410 ++++---------------- example/README.md | 38 ++ example/lib/main.dart | 2 +- example/lib/main_wav_stream.dart | 2 +- example/lib/page_3d_audio.dart | 4 +- example/lib/page_hello_flutter.dart | 2 +- example/lib/page_visualizer.dart | 4 +- example/lib/page_waveform.dart | 6 +- example/tests/tests.dart | 8 +- lib/fix_data.yaml | 22 ++ lib/flutter_soloud.dart | 1 - lib/src/audio_source.dart | 2 - lib/src/enums.dart | 20 +- lib/src/filter_params.dart | 28 +- lib/src/soloud.dart | 528 ++++++++++++++------------ lib/src/utils/assets_manager.dart | 1 + pubspec.yaml | 21 +- test_fixes/dispose_source.dart | 7 + test_fixes/dispose_source.dart.expect | 7 + 21 files changed, 520 insertions(+), 823 deletions(-) create mode 100644 test_fixes/dispose_source.dart create mode 100644 test_fixes/dispose_source.dart.expect diff --git a/CHANGELOG.md b/CHANGELOG.md index f7badf6..16f99d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,16 @@ ### 2.0.0-pre.X (XX XXX 2024) + - getLoopPoint now returns Duration +- Major changes to API docs and README +- Renamed `SoLoud.disposeSound` to `SoLoud.disposeSource`. + Quick fix available. +- Renamed `SoLoud.disposeAllSound` to `SoLoud.disposeAllSources`. + Quick fix available. +- Removed unused `AudioSource.keys` property. +- Switched LICENSE from Apache-2.0 to MIT. ### 2.0.0-pre.4 (21 Mar 2024) +- - some little fixes. ### 2.0.0-pre.3 (20 Mar 2024) diff --git a/LICENSE b/LICENSE index 175519d..1ee8fec 100755 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,19 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2023 Marco Bavagnoli - - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Copyright (c) 2024 Marco Bavagnoli + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 2a38936..013d1fa 100755 --- a/README.md +++ b/README.md @@ -1,335 +1,95 @@ -# Flutter low level audio plugin using SoLoud library - -Flutter low level audio plugin using SoLoud library with miniaudio backend and FFI. -It provides player, basic capture from microphone, 3D audio and more. - +A low-level audio plugin for Flutter. [![Pub Version](https://img.shields.io/pub/v/flutter_soloud?logo=dart)](https://pub.dev/packages/flutter_soloud) [![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis) |Linux|Windows|Android|MacOS|iOS|web| |:-:|:-:|:-:|:-:|:-:|:-:| -|💙|💙|💙|💙|💙|😭| - - -🌐 Supported on Linux, Windows, Mac, Android, and iOS - -🎤 **Player** and **capture** audio from microphone (no recording) - -🎶 **3D audio** with doppler effect - -🎚️ **Faders**, **oscillators** and audio effects like **echo**, **freeverb**, **robotizer**, **equalizer**, **bassboost** +|💙|💙|💙|💙|💙| WIP | -🎙️ **Multiple voices**, capable of playing different sounds simultaneously or even repeating the same sound multiple times on top of each other +### Select features: -💬 Includes a simple **speech synthesizer** +* Low latency, high performance +* Gapless looping (for background music, ambience, sound effects) +* Ability to load sounds to RAM, or play from disk +* Multiple voices, playing different or even the same sound + multiple times on top of each other +* Faders for attributes + (e.g. fade out for 2 seconds, then stop) +* 3D positional audio, including Doppler effect +* Support for MP3, WAV, OGG, and FLAC +* Audio effects such as echo, reverb, filter, equalizer -🔊 Supports various common formats such as 8, 16, and 32-bit WAVs, floating point **WAVs**, **OGG**, **MP3**, and **FLAC** - -🎚️ Enables **real-time** retrieval of audio **FFT** and **wave data** ## Overview -The ***flutter_soloud*** plugin utilizes a [forked](https://github.com/alnitak/soloud) repository of [SoLoud](https://github.com/jarikomppa/soloud), where the [miniaudio](https://github.com/mackron/miniaudio) audio backend has been updated and is located in src/soloud/src/backend/miniaudio - -For information regarding the SoLoud license, please refer to [this link](https://github.com/alnitak/soloud/blob/f4f089aa592aa45f5f6fa8c8efff64996fae920f/LICENSE). - - -There are 5 examples: -*(to use microphone on MacOs or iOS you should add audio input permission in the example app)* - -**The 1st** is a simple use-case to show how to play a sound and how to activate the capture. - -**The 2nd** aims to show a visualization of frequencies and wave data. -The file [**Visualizer.dart**] uses `getAudioTexture2D` to store new audio data into `audioData` on every tick. - -The video below illustrates how the data is then converted to an image (the upper widget) and sent to the shader (the middle widget). -The bottom widgets use FFT data on the left and wave data represented with a row of yellow vertical containers with the height taken from `audioData` on the right. +This plugin is mainly meant for games and immersive apps. +If you merely need to play audio (such as playing a single sound effect +or a non-looped song), and you don't need to worry about latency, +there are other Flutter plugins you can use, such as the popular +[`audioplayers` plugin](https://pub.dev/packages/audioplayers). -The `getAudioTexture2D` returns an array of 512x256. Each row contains 256 Floats of FFT data and 256 Floats of wave data, making it possible to write a shader like a spectrogram (shader #8) or a 3D visualization (shader #9). +> SoLoud is an easy to use, free, portable c/c++ audio engine for games. +> +> The engine has been designed to make simple things easy, +> while not making harder things impossible. +> +> [(from the underlying engine's homepage)](https://solhsa.com/soloud/index.html) -Shaders from 1 to 7 are using just 1 row of the `audioData`. Therefore, the texture generated to feed the shader should be 256x2 px. The 1st row represents the FFT data, and the 2nd represents the wave data. +The `flutter_soloud` plugin uses the +[SoLoud (C++) audio engine](https://solhsa.com/soloud/) +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. -Since many operations are required for each frame, the CPU and GPU can be under stress, leading to overheating of a mobile device. -It seems that sending an image (with `setImageSampler()`) to the shader is very expensive. You can observe this by disabling the shader widget. -https://github.com/alnitak/flutter_soloud/assets/192827/384c88aa-5daf-4f10-a879-169ab8522690 +## Example +The following example loads an MP3 asset, +plays it, then later stops it +and disposes of the audio source to reclaim memory. -***The 3rd*** example demonstrates how to manage sounds using their handles: every sound should be loaded before it can be played. Loading a sound can take some time and should not be done during gameplay, for instance, in a game. Once a sound is loaded, it can be played, and every instance of that same audio will be identified by its *handle*. - -The example shows how you can have background music and play a fire sound multiple times. - -https://github.com/alnitak/flutter_soloud/assets/192827/92c9db80-80ee-4a27-b6a9-3e089ffe600e - - -***The 4th*** example shows how to enance audio with 3D capabilities. There is a circle where the listener is placed in the center and a moving siren audio is represented by a little circle which is automatically animated or can be moved by mouse gesture. The sound volume fades off at the circonference. There is also a doppler effect that can be turned off. - -https://github.com/alnitak/flutter_soloud/assets/192827/f7cf9d71-be4f-4c83-99ff-89dbd9378859 - +```dart +void example() async { + final soloud = SoLoud.instance; -***The 5th*** example shows how to generete [**AudioSource**] key sounds. There is a handy tool method to generate the 12 key notes of a given octave. A widget to play them can be used with the touch or a keyboard. Different types of waveforms can be chosen including square,`saw`,`sin`,`triangle`,`bounce`,`jaws`,`humps`,`fSquare` and `fSaw`. -There are also simple knobs to adjust faders and oscillators. Other knobs to add/remove audio effects. + await soloud.init(); -https://github.com/alnitak/flutter_soloud/assets/192827/bfc5aa73-6dbc-42f5-90e4-bc1cc5e181e0 + final source = await soloud.loadAsset('path/to/asset.mp3'); + final handle = await soloud.play(source); + // ... -## Usage -#### The Player -First of all, *AudioIsolate* must be initialized: -``` -Future start() async{ - final value = SoLoud().startIsolate(); - if (value == PlayerErrors.noError) { - debugPrint('isolate started'); - return true; - } else { - debugPrint('isolate starting error: $value'); - return false; - } -} -``` -When succesfully started a sound can be loaded: -``` -Future loadSound(String completeFileName) { - final load = await SoLoud().loadFile(completeFileName); - if (load.error != PlayerErrors.noError) return null; - return load.sound; + await soloud.stop(handle); + await soloud.disposeSound(source); } ``` -There are 3 convenient methods that can be used instead in the [SoloudLoadingTool] class: -- ```Future loadFromAssets(String path)``` -- ```Future loadFromFile(String path)``` -- ```Future loadFromUrl(String url)``` +As you can see, most functionality in `flutter_soloud` is done through +calling methods on the `SoLoud` instance. +[**Read the API reference**](https://pub.dev/documentation/flutter_soloud/latest/flutter_soloud/SoLoud-class.html) +for the full listing of methods, and their documentation. -The [AudioSource] class: -``` -class AudioSource { - AudioSource(this.soundHash); +When you call a `loadAsset` (or `loadFile` or `loadUrl`) method, +in return you get an `AudioSource`. This is the reference to the sound +which is used by SoLoud. +The source needs to be disposed when it is needed no more. - // the [hash] returned by [loadFile] - final int soundHash; +Every time you play an `AudioSource`, you get a new `SoundHandle` +which uniquely identifies the new playing instance of the sound. +This handle is also added to the `AudioSource.handles` list so that you can +always check how many times any audio source is being played at the time. - /// handles of this sound. Multiple instances of this sound can be - /// played, each with their unique handle - List handle = []; +The `SoundHandle` also allows you to modify the currently-playing sounds, +such as changing their volume, pausing them, etc. - /// the user can listed ie when a sound ends or key events (TODO) - StreamController soundEvents = StreamController.broadcast(); -} -``` -*soundHash* and *handle* list are then used to call many methods in the *AudioIsolate()* class. +For more examples, check out the +[`example/` project](https://github.com/alnitak/flutter_soloud/tree/main/example) +included with the package. -**warning**: when you call a load* method, in return you will get a AudioSource. This is the reference to the sound which is used by SoLoud and need to be disposed when is no more needed. When you play a SoundsProps, intstead a new handle, to identify the new playing instance, is created and added to AudioSource.handle list. This let you play the sound as many times you want without calling a load* method again which can be laggy. -To dispose a sound call you should call *Soloud().disposeSound* or *Soloud().disposeAllSounds* -#### Capture from microphone - -Start the capture -``` -SoLoud().initCapture(); -SoLoud().startCapture(); -``` -now it's possible to get audio data. When the mic is no more needed, it can be stopped: -``` -SoLoud().stopCapture(); -``` -With the audio data it will be simple to do something like in the 1st example: - - -https://github.com/alnitak/flutter_soloud/assets/192827/b7d0343a-c646-4741-abab-bd88599212d0 - - - -### The AudioIsolate instance - -The `AudioIsolate` instance has the duty of receiving commands and sending them to a separate `Isolate`, while returning the results to the main UI isolate. - - -#### Player methods -| Function| Returns | Params| Description| -|---------|----------------------------------------------------------|---------|--------------------------------------------------------------------------------------------| -| **startIsolate**| PlayerErrors | -| Start the audio isolate and listen for messages coming from it.| -| **stopIsolate**| bool | -| Stop the loop, stop the engine, and kill the isolate. Must be called when there is no more need for the player or when closing the app.| -| **isIsolateRunning**| bool | -| Return true if the audio isolate is running.| -| **initEngine**| PlayerErrors | -| Initialize the audio engine. Defaults are: Sample rate 44100, buffer 2048, and Miniaudio audio backend.| -| **dispose**| - | -| Stop the audio engine.| -| **loadFile**| ({PlayerErrors error, AudioSource? sound}) |`String` fileName,
`LoadMode` mode = LoadMode.memory| Load a new sound to be played once or multiple times later.
If `mode = LoadMode.disk`, seek will have lags with MP3s.| -| **play**| ({PlayerErrors error, AudioSource sound, int newHandle}) | `int` soundHash, {
`double` volume = 1,
`double` pan = 0,
`bool` paused = false,
}| Play an already loaded sound identified by [sound].| -| **speechText**| ({PlayerErrors error, AudioSource sound}) | `String` textToSpeech| Speech from the given text.| -| **pauseSwitch**| PlayerErrors | `int` handle| Pause or unpause an already loaded sound identified by [handle].| -| **getPause**| ({PlayerErrors error, bool pause}) | `int` handle| Get the pause state of the sound identified by [handle].| -| **setRelativePlaySpeed**| PlayerErrors | `int` handle, `double` speed| Set a sound's relative play speed.| -| **getRelativePlaySpeed**| ({PlayerErrors error, double speed}) | `int` handle| Return the current play speed.| -| **stop**| PlayerErrors | `int` handle| Stop an already loaded sound identified by [handle] and clear it.| -| **disposeSound**| PlayerErrors | `int` handle| Stop ALL handles of the already loaded sound identified by [soundHash] and dispose it.| -| **getLooping**| ({PlayerErrors error, bool isLooping}) | -| Query whether a sound is set to loop.| -| **setLooping**| - | `int` handle, `bool` enable| This function can be used to set a sample to play on repeat, instead of just playing once.| -| **getLoopPoint**| ({PlayerErrors error, double time}) | -| Get sound loop point value.| -| **setLoopPoint**| PlayerErrors | `SoundHandle` handle, `double` time| Set sound loop point value.| -| **getLength**| ({PlayerErrors error, double length}) | `int` soundHash| Get the sound length in seconds.| -| **seek**| PlayerErrors | `int` handle, `double` time| Seek playing in seconds.
WARNING: when loading an MP3 file with `mode = LoadMode.disk`, the seek is laggy. This should not happens with FLACs, OGGs and WAVs.| -| **getPosition**| ({PlayerErrors error, double position}) | `int` handle| Get the current sound position in seconds.| -| **getVolume**| ({PlayerErrors error, double volume}) | `int` handle| Get current [handle] volume.| -| **setVolume**| ({PlayerErrors error, double volume}) | `int` handle, `double` volume| set [handle] volume.| -| **getIsValidVoiceHandle**| ({PlayerErrors error, bool isValid}) | `int` handle| Check if a handle is still valid.| -| **setVisualizationEnabled**| - | `bool` enabled| Enable or disable getting data from `getFft`, `getWave`, `getAudioTexture*`.| -| **getVisualizationEnabled**| ({PlayerErrors error, bool isEnabled}) | -| Get the state of visualization flag.| -| **getFft**| - | `Pointer` fft| Returns a 256 float array containing FFT data.| -| **getWave**| - | `Pointer` wave| Returns a 256 float array containing wave data (magnitudes).| -| **getAudioTexture**| - | `Pointer` samples| Returns 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].| -| **getAudioTexture2D**| - | `Pointer>` samples| Return a floats matrix of 256x512.
Every row is composed of 256 FFT values plus 256 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 will be lost).| -| **setFftSmoothing**| - | `double` smooth| 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 in a less shaky visualization.
0 = no smooth
1 = full smooth
The new value is calculated with:
`newFreq = smooth * oldFreq + (1 - smooth) * newFreq`| - - -#### Waveform -| Function| Returns | Params | Description| -|---------|--------------------------------------------|---------------------------------------------------------------------------------|------------| -| **loadWaveform**| ({PlayerErrors error, AudioSource? sound}) | `WaveForm` waveform
`bool` superWave
`double` scale
`double` detune | Load a new sound to be played.| -| **setWaveform**| PlayerErrors | `AudioSource` sound
`WaveForm` newWaveform | Set a new waveform for the [sound].| -| **setWaveformScale**| PlayerErrors | `SoundPropsAudioSource` sound
`double` newScale | Set a new scale for the [sound] (only if [superWave] is true).| -| **setWaveformDetune**| PlayerErrors | `SoundPropsAudioSource` sound
`double` newDetune | Set a new detune for the [sound] (only if [superWave] is true).| -| **setWaveformFreq**| PlayerErrors | `SoundPropsAudioSource` sound
`double` newFreq | Set a new frequency for the [sound].| -| **setWaveformSuperWave**| PlayerErrors | `SoundPropsAudioSource` sound
`bool` superwave | Set to compute superwave for the [sound].| - -**enum WaveForm** -| Name| Description| -|---------|--------| -|**square**|Raw, harsh square wave| -|**saw**|Raw, harsh saw wave| -|**sin**|Sine wave| -|**triangle**|Triangle wave| -|**bounce**|Bounce, i.e, abs(sin())| -|**jaws**|Quater sine wave, rest of period quiet| -|**humps**|Half sine wave, rest of period quiet| -|**fSquare**|"Fourier" square wave; less noisy| -|**fSaw**|"Fourier" saw wave; less noisy| - - -#### Audio FXs, faders and oscillators methods -These methods add audio effects to sounds. -Faders and oscillators are binded to sound handles, so they need [AudioSource.handle] as first parameter. -Audio FXs like *echo*, *freeverb*, *bassboost* etc, are working on the output, so they can set anytime while playing something. - -| Function| Returns| Params| Description| -|---------|---------|---------|---------| -|**fadeGlobalVolume**| PlayerErrors error|`double` to,
`double` time|Smoothly change the global volume over specified time.| -|**fadeVolume**| PlayerErrors error|`int` handle,
`double` to,
`double` time|Smoothly change a channel's volume over specified time.| -|**fadePan**| PlayerErrors error|`int` handle,
`double` to,
`double` time|Smoothly change a channel's pan setting over specified time.| -|**fadeRelativePlaySpeed**| PlayerErrors error|`int` handle,
`double` to,
`double` time|Smoothly change a channel's relative play speed over specified time.| -|**schedulePause**| PlayerErrors error|`int` handle,
`double` time|After specified time, pause the channel.| -|**scheduleStop**| PlayerErrors error|`int` handle,
`double` time|After specified time, stop the channel.| -|**oscillateVolume**| PlayerErrors error|`int` handle,
`double` from,
`double` to,
`double` time|Smoothly change a channel's pan setting over specified time.| -|**oscillatePan**| PlayerErrors error|`int` handle,
`double` from,
`double` to,
`double` time|Set fader to oscillate the panning at specified frequency.| -|**oscillateRelativePlaySpeed**| PlayerErrors error|`int` handle,
`double` from,
`double` to,
`double` time|Set fader to oscillate the relative play speed at specified frequency.| -|**oscillateGlobalVolume**| PlayerErrors error|`double` from,
`double` to,
`double` time|Set fader to oscillate the global volume at specified frequency.| -|**isFilterActive**| ({PlayerErrors error, int index})|`FilterType` filterType|Check if the given filter is active or not.| -|**getFilterParamNames**| ({PlayerErrors error, List names})|`FilterType` filterType|Get parameters names of the given filter.| -|**addGlobalFilter**| PlayerErrors|`FilterType` filterType|Add the filter [filterType].| -|**removeGlobalFilter**| PlayerErrors|`FilterType` filterType|Remove the filter [filterType].| -|**setFxParams**| PlayerErrors|`FilterType` filterType,
`int` attributeId,
`double` value|Set the effect parameter with id [attributeId] of [filterType] with [value] value.| -|**getFxParams**| PlayerErrors|`FilterType` filterType,
`int` attributeId|Get the effect parameter with id [attributeId] of [filterType].| - - -**enum FilterType** -| Name| -|---------| -|**biquadResonantFilter**| -|**eqFilter**| -|**echoFilter**| -|**lofiFilter**| -|**flangerFilter**| -|**bassboostFilter**| -|**waveShaperFilter**| -|**robotizeFilter**| -|**freeverbFilter**| - -There are also conveninet const to easily access effect parameter like *filter name*, *param names*, *mins values*, *max values* and *defaults*: - -`fxEq`, `fxEcho`, `fxLofi`, `fxFlanger`, `fxBassboost`, `fxWaveShaper`, `fxRobotize`, `fxFreeverb`. - - - -#### 3D audio methods -| Function| Returns| Params| Description| -|---------|---------|---------|---------| -| **play3d**| `int` handle| `int` soundHash, `double` posX, `double` posY, `double` posZ,
{`double` velX = 0,
`double` velY = 0,
`double` velZ = 0,
`double` volume = 1,
`bool` paused = false}| play3d() is the 3d version of the play() call. Returns the handle of the sound, 0 if error| -| **set3dSoundSpeed**| -| `double` speed| 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.| -| **get3dSoundSpeed**| `double`| -| Get the sound speed.| -| **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, at-vector, up-vector and velocity parameters of the 3d audio listener with one call.| -| **set3dListenerPosition**| -| `double` posX,
`double` posY,
`double` posZ| Get the sound speed.| -| **set3dListenerAt**| -| `double` atX,
`double` atY,
`double` atZ| You can set the "at" vector parameter of the 3d audio listener.| -| **set3dListenerUp**| -| `double` upX,
`double` upY,
`double` upZ| You can set the "up" vector parameter of the 3d audio listener.| -| **set3dListenerVelocity**| -| `double` velocityX,
`double` velocityY,
`double` velocityZ| You can set the listener's velocity vector parameter.| -| **set3dSourceParameters**| -| `int` handle,
`double` posX,
`double` posY,
`double` posZ,
`double` velocityX,
`double` velocityY,
`double` velocityZ| You can set the position and velocity parameters of a live 3d audio source with one call.| -| **set3dSourcePosition**| -| `int` handle,
`double` posX,
`double` posY,
`double` posZ| You can set the position parameters of a live 3d audio source.| -| **set3dSourceVelocity**| -| `int` handle,
`double` velocityX,
`double` velocityY,
`double` velocityZ| You can set the velocity parameters of a live 3d audio source.| -| **set3dSourceMinMaxDistance**| -| `int` handle,
`double` minDistance,
`double` maxDistance| You can set the minimum and maximum distance parameters of a live 3d audio source.| -| **set3dSourceAttenuation**| -| `int` handle,
`int` attenuationModel,
`double` attenuationRolloffFactor| You can change the attenuation model and rolloff factor parameters of a live 3d audio source.
See https://solhsa.com/soloud/concepts3d.html | -| **set3dSourceDopplerFactor**| -| `int` handle,
`double` dopplerFactor| You can change the doppler factor of a live 3d audio source.
See https://solhsa.com/soloud/concepts3d.html | - - - -The `PlayerErrors` enum: -|name|description| -|---|---| -|***noError***|No error| -|***invalidParameter***|Some parameter is invalid| -|***fileNotFound***|File not found| -|***fileLoadFailed***|File found, but could not be loaded| -|***dllNotFound***|DLL not found, or wrong DLL| -|***outOfMemory***|Out of memory| -|***notImplemented***|Feature not implemented| -|***unknownError***|Other error| -|***backendNotInited***|Player not initialized| -|***nullPointer***|null pointer. Could happens when passing a non initialized pointer (with calloc()) to retrieve FFT or wave data| -|***soundHashNotFound***|The sound with specified hash is not found| -|***fileAlreadyLoaded***|The sound file has already been loaded| -|***isolateAlreadyStarted***|Audio isolate already started| -|***isolateNotStarted***|Audio isolate not yet started| -|***engineNotInited***|Engine not yet started| - -*AudioIsolate()* has a `StreamController` which can be used, for now, only to know when a sound handle reached the end: -``` -StreamSubscription? _subscription; -void listedToEndPlaying(AudioSource sound) { - _subscription = sound!.soundEvents.stream.listen( - (event) { - /// Here the [event.handle] of [sound] has naturally finished - /// and [sound.handle] doesn't contains [envent.handle] anymore. - /// Not passing here when calling [SoLoud().stop()] - /// or [SoLoud().disposeSound()] - }, - ); -} -``` -it has also a `StreamController` to monitor when the engine starts or stops: -``` -SoLoud().audioEvent.stream.listen( - (event) { - /// event is of [AudioEvent] enum type: - /// [AudioEvent.isolateStarted] the player is started and sounds can be played - /// [AudioEvent.isolateStopped] player stopped - /// [captureStarted] microphone is active and audio data can be read - /// [captureStopped] microphone stopped - }, -); -``` - -#### Capture methods -| Function| Returns| Params| Description| -|---------|---------|---------|--------------------------------------------------------------------------------------------| -| **listCaptureDevices**| CaptureDevice| - | List available input devices. Useful on desktop to choose which input device to use.| -| **initCapture**| CaptureErrors| - | Initialize input device with [deviceID]
Return [CaptureErrors.captureNoError] if no error.| -| **isCaptureInitialized**| bool| - | Get the status of the device.| -| **isCaptureStarted**| bool| - | Returns true if the device is capturing audio.| -| **stopCapture**| CaptureErrors| - | Stop and deinit capture device.| -| **startCapture**| CaptureErrors| - | Start capturing audio data.| -| **getCaptureAudioTexture2D**| CaptureErrors| - | 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).| -| **setCaptureFftSmoothing**| CaptureErrors| `double` smooth | 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 resul 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| - -#### Logging +## Logging The `flutter_soloud` package logs everything (from severe warnings to fine debug messages) using the standard @@ -369,6 +129,19 @@ See the `logging` package's [documentation](https://pub.dev/packages/logging) to learn more about its functionality. +## License + +The Dart plugin is covered by the MIT license. + +For information regarding the license for the underlying SoLoud (C++) engine, +please refer to [this link](https://solhsa.com/soloud/legal.html). +In short, the SoLoud code itself is covered by +the ZLib/LibPNG license +(which is [compatible](https://en.wikipedia.org/wiki/Zlib_License) with GNU GPL). +Some modules (such as MP3 or OGG support) are covered with other, but still +permissive open source licenses. + + ## Contribute To use native code, bindings from Dart to C/C++ are needed. To avoid writing these manually, they are generated from the header file (`src/ffi_gen_tmp.h`) using [package:ffigen](https://pub.dev/packages/ffigen) and temporarily stored in `lib/flutter_soloud_FFIGEN.dart`. You can generate the bindings by running `dart run ffigen`. @@ -380,9 +153,6 @@ Since I needed to modify the generated `.dart` file, I followed this flow: Additionally, I have forked the [SoLoud](https://github.com/jarikomppa/soloud) repository and made modifications to include the latest [Miniaudio](https://github.com/mackron/miniaudio) audio backend. This backend is in the [new_miniaudio] branch of my [fork](https://github.com/alnitak/soloud) and is set as the default. - - - #### Project structure This plugin uses the following structure: @@ -393,6 +163,11 @@ This plugin uses the following structure: * `src/soloud`: contains the SoLoud sources of my fork +The `flutter_soloud` plugin utilizes a [forked](https://github.com/alnitak/soloud) +repository of [SoLoud](https://github.com/jarikomppa/soloud), +where the [miniaudio](https://github.com/mackron/miniaudio) audio backend has been updated and +is located in `src/soloud/src/backend/miniaudio`. + #### Debugging I have provided the necessary settings in the **.vscode** directory for debugging native C++ code on both Linux and Windows. To debug on Android, please use Android Studio and open the project located in the ***example/android*** directory. However, I am not familiar with the process of debugging native code on Mac and iOS. @@ -438,30 +213,7 @@ On the simulator, the Impeller engine doesn't work (20 Lug 2023). To disable it, Unfortunately, I don't have a real device to test it. #### Web -I put in a lot of effort to make this to work on the web! :( -I have successfully compiled the sources with Emscripten. Inside the **web** directory, there's a script to automate the compiling process using the `CmakeLists.txt` file. This will generate **libflutter_soloud_web_plugin.wasm** and **libflutter_soloud_web_plugin.bc**. - -Initially, I tried using the [wasm_interop](https://pub.dev/packages/wasm_interop) plugin, but encountered errors while loading and initializing the Module. - -Then, I attempted using [web_ffi](https://pub.dev/packages/web_ffi), but it seems to have been discontinued because it only supports the old `dart:ffi API 2.12.0`, which cannot be used here. - - -## TODOs - -Many things can still be done. - -- Record from microphone to audio file. -- Since the `load` audio file feature uses the memory to store all audio data (see [#26](https://github.com/alnitak/flutter_soloud/issues/26)) to have no lags when start playing (useful with game sounds), make it possible to use `Soloud::wavStream` -- The FFT data doesn't match my expectations. Some work still needs to be done on *Analyzer::calcFFT()* in `src/analyzer.cpp`. - -|![spectrum1](/img/flutter_soloud_spectrum.png)|![spectrum2](/img/audacity_spectrum.png)| -|:--|:--| -|*flutter_soloud spectrum*|*audacity spectrum*| +Work on web support (using WASM) is tracked in +https://github.com/alnitak/flutter_soloud/issues/46. -For now, only a small portion of the possibilities offered by SoLoud have been implemented. Look [here](https://solhsa.com/soloud/index.html). -* audio filter effects -* 3D audio ✅ -* TED and SID soundchip simulator (Commodore 64/plus) -* noise and waveform generation ✅ -and much more I think! diff --git a/example/README.md b/example/README.md index 4ae469f..1ef4ea8 100755 --- a/example/README.md +++ b/example/README.md @@ -1,3 +1,41 @@ # flutter_soloud_example Demonstrates how to use the `flutter_soloud` plugin. + +There are 5 examples: +*(to use microphone on MacOs or iOS you should add audio input permission in the example app)* + +**The 1st** is a simple use-case to show how to play a sound and how to activate the capture. + +**The 2nd** aims to show a visualization of frequencies and wave data. +The file [**Visualizer.dart**] uses `getAudioTexture2D` to store new audio data into `audioData` on every tick. + +The video below illustrates how the data is then converted to an image (the upper widget) and sent to the shader (the middle widget). +The bottom widgets use FFT data on the left and wave data represented with a row of yellow vertical containers with the height taken from `audioData` on the right. + +The `getAudioTexture2D` returns an array of 512x256. Each row contains 256 Floats of FFT data and 256 Floats of wave data, making it possible to write a shader like a spectrogram (shader #8) or a 3D visualization (shader #9). + +Shaders from 1 to 7 are using just 1 row of the `audioData`. Therefore, the texture generated to feed the shader should be 256x2 px. The 1st row represents the FFT data, and the 2nd represents the wave data. + +Since many operations are required for each frame, the CPU and GPU can be under stress, leading to overheating of a mobile device. +It seems that sending an image (with `setImageSampler()`) to the shader is very expensive. You can observe this by disabling the shader widget. + +https://github.com/alnitak/flutter_soloud/assets/192827/384c88aa-5daf-4f10-a879-169ab8522690 + + +***The 3rd*** example demonstrates how to manage sounds using their handles: every sound should be loaded before it can be played. Loading a sound can take some time and should not be done during gameplay, for instance, in a game. Once a sound is loaded, it can be played, and every instance of that same audio will be identified by its *handle*. + +The example shows how you can have background music and play a fire sound multiple times. + +https://github.com/alnitak/flutter_soloud/assets/192827/92c9db80-80ee-4a27-b6a9-3e089ffe600e + + +***The 4th*** example shows how to enance audio with 3D capabilities. There is a circle where the listener is placed in the center and a moving siren audio is represented by a little circle which is automatically animated or can be moved by mouse gesture. The sound volume fades off at the circonference. There is also a doppler effect that can be turned off. + +https://github.com/alnitak/flutter_soloud/assets/192827/f7cf9d71-be4f-4c83-99ff-89dbd9378859 + + +***The 5th*** example shows how to generete [**AudioSource**] key sounds. There is a handy tool method to generate the 12 key notes of a given octave. A widget to play them can be used with the touch or a keyboard. Different types of waveforms can be chosen including square,`saw`,`sin`,`triangle`,`bounce`,`jaws`,`humps`,`fSquare` and `fSaw`. +There are also simple knobs to adjust faders and oscillators. Other knobs to add/remove audio effects. + +https://github.com/alnitak/flutter_soloud/assets/192827/bfc5aa73-6dbc-42f5-90e4-bc1cc5e181e0 diff --git a/example/lib/main.dart b/example/lib/main.dart index 6f8b97e..4e5df37 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -90,7 +90,7 @@ class MyHomePage extends StatelessWidget { child: TabBar( isScrollable: true, onTap: (value) { - SoLoud.instance.disposeAllSound(); + SoLoud.instance.disposeAllSources(); }, tabs: const [ Tab(text: 'hello world!'), diff --git a/example/lib/main_wav_stream.dart b/example/lib/main_wav_stream.dart index d98cfdc..30a74db 100644 --- a/example/lib/main_wav_stream.dart +++ b/example/lib/main_wav_stream.dart @@ -81,7 +81,7 @@ class _MyHomePageState extends State { isStarted = true; } - await SoLoud.instance.disposeAllSound(); + await SoLoud.instance.disposeAllSources(); sounds.clear(); seekPos.value = Duration.zero; diff --git a/example/lib/page_3d_audio.dart b/example/lib/page_3d_audio.dart index 572a682..51f176f 100644 --- a/example/lib/page_3d_audio.dart +++ b/example/lib/page_3d_audio.dart @@ -61,7 +61,7 @@ class _Page3DAudioState extends State { /// stop any previous sound loaded if (currentSound != null) { try { - await SoLoud.instance.disposeSound(currentSound!); + await SoLoud.instance.disposeSource(currentSound!); } catch (e) { _log.severe('error disposing sound: $e'); return; @@ -87,7 +87,7 @@ class _Page3DAudioState extends State { /// stop any previous sound loaded if (currentSound != null) { try { - await SoLoud.instance.disposeSound(currentSound!); + await SoLoud.instance.disposeSource(currentSound!); } catch (e) { _log.severe('error disposing sound: $e'); return; diff --git a/example/lib/page_hello_flutter.dart b/example/lib/page_hello_flutter.dart index a476977..1ed3a77 100644 --- a/example/lib/page_hello_flutter.dart +++ b/example/lib/page_hello_flutter.dart @@ -86,7 +86,7 @@ class _PageHelloFlutterSoLoudState extends State { /// stop any previous sound loaded if (currentSound != null) { try { - await SoLoud.instance.disposeSound(currentSound!); + await SoLoud.instance.disposeSource(currentSound!); } catch (e) { _log.severe('dispose error', e); return; diff --git a/example/lib/page_visualizer.dart b/example/lib/page_visualizer.dart index 3c1ea87..27d645b 100644 --- a/example/lib/page_visualizer.dart +++ b/example/lib/page_visualizer.dart @@ -470,7 +470,7 @@ class _PageVisualizerState extends State { Future play(String file) async { if (currentSound != null) { try { - await SoLoud.instance.disposeSound(currentSound!); + await SoLoud.instance.disposeSource(currentSound!); } catch (e) { _log.severe('error disposing the current sound', e); return; @@ -496,7 +496,7 @@ class _PageVisualizerState extends State { /// It's needed to call dispose when it end else it will /// not be cleared - SoLoud.instance.disposeSound(currentSound!); + SoLoud.instance.disposeSource(currentSound!); currentSound = null; }, ); diff --git a/example/lib/page_waveform.dart b/example/lib/page_waveform.dart index cd80732..7f7ce77 100644 --- a/example/lib/page_waveform.dart +++ b/example/lib/page_waveform.dart @@ -50,7 +50,7 @@ class _PageWaveformState extends State { } Future setupNotes() async { - await SoLoud.instance.disposeAllSound(); + await SoLoud.instance.disposeAllSources(); notes = await SoLoudTools.createNotes( octave: octave, superwave: superWave, @@ -79,7 +79,7 @@ class _PageWaveformState extends State { ElevatedButton( onPressed: () async { if (sound != null) { - await SoLoud.instance.disposeSound(sound!); + await SoLoud.instance.disposeSource(sound!); } /// text created by ChatGPT :) @@ -109,7 +109,7 @@ class _PageWaveformState extends State { ElevatedButton( onPressed: () { if (sound != null) { - SoLoud.instance.disposeSound(sound!).then((value) { + SoLoud.instance.disposeSource(sound!).then((value) { sound = null; }); } diff --git a/example/tests/tests.dart b/example/tests/tests.dart index 0b0c00d..24e09a7 100644 --- a/example/tests/tests.dart +++ b/example/tests/tests.dart @@ -152,7 +152,7 @@ Future testAllInstancesFinished() async { final log = Logger('testAllInstancesFinished'); await initialize(); - await SoLoud.instance.disposeAllSound(); + await SoLoud.instance.disposeAllSources(); assert( SoLoud.instance.activeSounds.isEmpty, 'Active sounds even after disposeAllSound()', @@ -169,14 +169,14 @@ Future testAllInstancesFinished() async { unawaited( explosion.allInstancesFinished.first.then((_) async { log.info('All instances of explosion finished.'); - await SoLoud.instance.disposeSound(explosion); + await SoLoud.instance.disposeSource(explosion); explosionDisposed = true; }), ); unawaited( song.allInstancesFinished.first.then((_) async { log.info('All instances of song finished.'); - await SoLoud.instance.disposeSound(song); + await SoLoud.instance.disposeSource(song); songDisposed = true; }), ); @@ -486,7 +486,7 @@ Future delay(int ms) async { Future loadAsset() async { if (currentSound != null) { - await SoLoud.instance.disposeSound(currentSound!); + await SoLoud.instance.disposeSource(currentSound!); } currentSound = await SoLoud.instance.loadAsset('assets/audio/explosion.mp3'); diff --git a/lib/fix_data.yaml b/lib/fix_data.yaml index d62a638..7a5dbad 100644 --- a/lib/fix_data.yaml +++ b/lib/fix_data.yaml @@ -9,6 +9,28 @@ version: 1 transforms: + # SoLoud.disposeAllSound => SoLoud.disposeAllSources + - title: "Rename to 'disposeAllSources'" + date: 2024-04-03 + element: + uris: [ 'flutter_soloud.dart', 'package:flutter_soloud/flutter_soloud.dart' ] + method: 'disposeAllSound' + inClass: 'SoLoud' + changes: + - kind: 'rename' + newName: 'disposeAllSources' + + # SoLoud.disposeSound => SoLoud.disposeSource + - title: "Rename to 'disposeSource'" + date: 2024-04-03 + element: + uris: [ 'flutter_soloud.dart', 'package:flutter_soloud/flutter_soloud.dart' ] + method: 'disposeSound' + inClass: 'SoLoud' + changes: + - kind: 'rename' + newName: 'disposeSource' + # SoLoud.initialize => SoLoud.init - title: "Rename to 'init'" date: 2024-03-20 diff --git a/lib/flutter_soloud.dart b/lib/flutter_soloud.dart index 40e499c..f502aff 100644 --- a/lib/flutter_soloud.dart +++ b/lib/flutter_soloud.dart @@ -1,5 +1,4 @@ /// Flutter low level audio plugin using SoLoud library and FFI -/// library flutter_soloud; export 'src/audio_source.dart'; diff --git a/lib/src/audio_source.dart b/lib/src/audio_source.dart index 59bfe3f..f21deb2 100644 --- a/lib/src/audio_source.dart +++ b/lib/src/audio_source.dart @@ -67,9 +67,7 @@ class AudioSource { @internal final Set handlesInternal = {}; - /// // TODO(marco): make marker keys time able to trigger an event - final List keys = []; /// Backing controller for [soundEvents]. @internal diff --git a/lib/src/enums.dart b/lib/src/enums.dart index e2a0a33..61a6e78 100644 --- a/lib/src/enums.dart +++ b/lib/src/enums.dart @@ -159,33 +159,33 @@ enum PlayerErrors { String toString() => 'PlayerErrors.$name ($_asSentence)'; } -/// Wave forms +/// The types of waveforms. enum WaveForm { - /// Raw, harsh square wave + /// Raw, harsh square wave. square, - /// Raw, harsh saw wave + /// Raw, harsh saw wave. saw, - /// Sine wave + /// Sine wave. sin, - /// Triangle wave + /// Triangle wave. triangle, - /// Bounce, i.e, abs(sin()) + /// Bounce, i.e, abs(sin()). bounce, - /// Quarter sine wave, rest of period quiet + /// Quarter sine wave, rest of period quiet. jaws, - /// Half sine wave, rest of period quiet + /// Half sine wave, rest of period quiet. humps, - /// "Fourier" square wave; less noisy + /// "Fourier" square wave; less noisy. fSquare, - /// "Fourier" saw wave; less noisy + /// "Fourier" saw wave; less noisy. fSaw, } diff --git a/lib/src/filter_params.dart b/lib/src/filter_params.dart index 7c48cd5..001ec90 100644 --- a/lib/src/filter_params.dart +++ b/lib/src/filter_params.dart @@ -1,34 +1,34 @@ -/// Filters enum +/// The different types of audio filters. enum FilterType { - /// + /// A biquad resonant filter. biquadResonantFilter, - /// + /// An equalizer filter. eqFilter, - /// + /// An echo filter. echoFilter, - /// + /// A lo-fi filter. lofiFilter, - /// + /// A flanger filter. flangerFilter, - /// + /// A bass-boost filter. bassboostFilter, - /// + /// A wave shaper filter. waveShaperFilter, - /// + /// A robotize filter. robotizeFilter, - /// + /// A reverb filter. freeverbFilter, } -/// +/// The parameters for each filter. typedef FxParams = ({ String title, List names, @@ -76,7 +76,7 @@ const FxParams fxEcho = ( defs: [1, 0.3, 0.7, 0], ); -/// Lofi filter +/// Lo-fi filter const FxParams fxLofi = ( title: 'Lofi', names: ['Wet', 'Samplerate', 'Bitdepth'], @@ -94,7 +94,7 @@ const FxParams fxFlanger = ( defs: [1, 0.005, 10], ); -/// Bassboost filter +/// Bass-boost filter const FxParams fxBassboost = ( title: 'Bassboost', names: ['Wet', 'Boost'], @@ -123,7 +123,7 @@ const FxParams fxRobotize = ( defs: [1, 30, 0], ); -/// Freeverb filter +/// Freeverb (reverb) filter const FxParams fxFreeverb = ( title: 'Freeverb', diff --git a/lib/src/soloud.dart b/lib/src/soloud.dart index 6a853fb..cc49cfe 100644 --- a/lib/src/soloud.dart +++ b/lib/src/soloud.dart @@ -20,23 +20,26 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; -/// The events exposed by the plugin +/// The events exposed by the plugin. enum AudioEvent { - /// emitted when audio isolate is started + /// Emitted when audio isolate is started. isolateStarted, - /// emitted when audio isolate is stopped + /// Emitted when audio isolate is stopped. isolateStopped, - /// emitted when audio capture is started + /// Emitted when audio capture is started. captureStarted, - /// emitted when audio capture is stopped + /// Emitted when audio capture is stopped. captureStopped, } /// The main class to call all the audio methods that play sounds. /// +/// This class has a singleton [instance] which represents the (also singleton) +/// instance of the SoLoud (C++) engine. +/// /// For methods that _capture_ sounds, use [SoLoudCapture]. interface class SoLoud { /// A deprecated way to access the singleton [instance] of SoLoud. @@ -217,7 +220,8 @@ interface class SoLoud { /// more general initialization process, this field is only an internal /// control mechanism. Users should use [initialized] instead. /// - /// The field is useful in [disposeAllSound], which is called from `shutdown` + /// The field is useful in [disposeAllSources], + /// which is called from `shutdown` /// (so [isInitialized] is already `false` at that point). /// // TODO(filiph): check if still needed @@ -233,7 +237,7 @@ interface class SoLoud { /// Backing of [activeSounds]. final List _activeSounds = []; - /// The sounds that are currently _playing_. + /// The sounds that are _currently being played_. Iterable get activeSounds => _activeSounds; /// Wait for the isolate to return after the event has been completed. @@ -489,7 +493,7 @@ interface class SoLoud { } /// Stops the engine and disposes of all resources, including sounds - /// and the audio isolate in an synchronous way. + /// and the audio isolate in a synchronous way. /// /// This method is meant to be called when exiting the app. For example /// within the `dispose()` of the uppermost widget in the tree @@ -631,7 +635,7 @@ interface class SoLoud { _log.finest('_disposeEngine() called'); if (_isolate == null || !_isEngineInitialized) return false; - await disposeAllSound(); + await disposeAllSources(); /// first stop the loop await _stopLoop(); @@ -651,19 +655,24 @@ interface class SoLoud { /// Load a new sound to be played once or multiple times later, from /// the file system. /// - /// [completeFileName] the complete file path. - /// [LoadMode] if `LoadMode.memory`, the whole uncompressed RAW PCM + /// Provide the complete [path] of the file to be played. + /// + /// When [mode] is [LoadMode.memory], the whole uncompressed RAW PCM /// audio is loaded into memory. Used to prevent gaps or lags /// when seeking/starting a sound (less CPU, more memory allocated). - /// If `LoadMode.disk` is used, the audio data is loaded + /// 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] = `LoadMode.disk`. - /// Default is `LoadMode.memory`. + /// See the [seek] note problem when using [LoadMode.disk]. + /// The default is [LoadMode.memory]. + /// /// Returns the new sound as [AudioSource]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. + /// + /// If the file is already loaded, this is a no-op (but a warning + /// will be produced in the log). Future loadFile( - String completeFileName, { + String path, { LoadMode mode = LoadMode.memory, }) async { if (!isInitialized) { @@ -672,12 +681,12 @@ interface class SoLoud { _mainToIsolateStream?.send( { 'event': MessageEvents.loadFile, - 'args': (completeFileName: completeFileName, mode: mode), + 'args': (completeFileName: path, mode: mode), }, ); final ret = (await _waitForEvent( MessageEvents.loadFile, - (completeFileName: completeFileName, mode: mode), + (completeFileName: path, mode: mode), )) as ({PlayerErrors error, AudioSource? sound}); _logPlayerError(ret.error, from: 'loadFile() result'); @@ -687,7 +696,7 @@ interface class SoLoud { _activeSounds.add(ret.sound!); return ret.sound!; } else if (ret.error == PlayerErrors.fileAlreadyLoaded) { - _log.warning(() => "Sound '$completeFileName' was already loaded. " + _log.warning(() => "Sound '$path' was already loaded. " 'Prefer loading only once, and reusing the loaded sound ' 'when playing.'); // The `audio_isolate.dart` code has logic to find the already-loaded @@ -722,6 +731,9 @@ interface class SoLoud { /// Throws [SoLoudNotInitializedException] if the engine is not initialized. /// /// Returns the new sound as [AudioSource]. + /// + /// If the file is already loaded, this is a no-op (but a warning + /// will be produced in the log). Future loadAsset( String key, { LoadMode mode = LoadMode.memory, @@ -757,6 +769,9 @@ interface class SoLoud { /// Throws [SoLoudNotInitializedException] if the engine is not initialized. /// /// Returns the new sound as [AudioSource]. + /// + /// If the file is already loaded, this is a no-op (but a warning + /// will be produced in the log). Future loadUrl( String url, { LoadMode mode = LoadMode.memory, @@ -771,12 +786,13 @@ interface class SoLoud { return loadFile(file.absolute.path, mode: mode); } - /// Load a new waveform to be played once or multiple times later + /// Load a new waveform to be played once or multiple times later. + /// + /// Specify the type of the waveform (such as sine or square or saw) + /// with [waveform]. /// - /// [waveform] the type of [WaveForm] to generate. - /// [superWave] whater this is a superWave. - /// [scale] if using [superWave] this is its scale. - /// [detune] if using [superWave] this is its detune. + /// You must also specify if the waveform should be a [superWave], + /// and what the superwave's [scale] and [detune] should be. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. /// @@ -821,8 +837,8 @@ interface class SoLoud { /// Set a waveform type to the given sound: see [WaveForm] enum. /// - /// [sound] the sound to change the wafeform type. - /// [newWaveform] the new waveform type. + /// Provide the [sound] for which to change the waveform type, + /// and the new [newWaveform]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setWaveform(AudioSource sound, WaveForm newWaveform) { @@ -834,8 +850,8 @@ interface class SoLoud { /// If this sound is a `superWave` you can change the scale at runtime. /// - /// [sound] the sound to change the scale to. - /// [newScale] the new scale. + /// Provide the [sound] for which to change the scale, + /// and the new [newScale]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setWaveformScale(AudioSource sound, double newScale) { @@ -847,8 +863,8 @@ interface class SoLoud { /// If this sound is a `superWave` you can change the detune at runtime. /// - /// [sound] the sound to change the detune to. - /// [newDetune] the new detune. + /// Provide the [sound] for which to change the detune, + /// and the new [newDetune]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setWaveformDetune(AudioSource sound, double newDetune) { @@ -858,10 +874,10 @@ interface class SoLoud { SoLoudController().soLoudFFI.setWaveformDetune(sound.soundHash, newDetune); } - /// Set the frequency of the given sound. + /// Set the frequency of the given waveform sound. /// - /// [sound] the sound to se the [newFrequency] to. - /// [newFrequency] the new frequency. + /// Provide the [sound] for which to change the scale, + /// and the new [newFrequency]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setWaveformFreq(AudioSource sound, double newFrequency) { @@ -871,10 +887,10 @@ interface class SoLoud { SoLoudController().soLoudFFI.setWaveformFreq(sound.soundHash, newFrequency); } - /// Set the given sound as a super wave. + /// Set the given waveform sound's super wave flag. /// - /// [sound] the sound to se the [superwave] to. - /// [superwave] whether this sound should be a super wave or not. + /// Provide the [sound] for which to change the flag, + /// and the new [superwave] value. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setWaveformSuperWave(AudioSource sound, bool superwave) { @@ -887,9 +903,8 @@ interface class SoLoud { ); } - /// Speech the given text. + /// Create a new audio source from the given [textToSpeech]. /// - /// [textToSpeech] the text to be spoken. /// Returns the new sound as [AudioSource]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. @@ -915,13 +930,28 @@ interface class SoLoud { throw SoLoudCppException.fromPlayerError(ret.error); } - /// Play already loaded sound identified by [sound] + /// Play an already-loaded sound identified by [sound]. Creates a new + /// playing instance of the sound, and returns its [SoundHandle]. /// - /// [sound] the sound to play - /// [volume] 1.0 full volume - /// [pan] 0.0 centered - /// [paused] 0 not pause - /// Returns the [SoundHandle] of this new sound. + /// You can provide the [volume], where `1.0` is full volume and `0.0` + /// is silent. Defaults to `1.0`. + /// + /// You can provide [pan] for the sound, with `0.0` centered, + /// `-1.0` fully left, and `1.0` fully right. Defaults to `0.0`. + /// + /// Set [paused] to `true` if you want the new sound instance to + /// start paused. This is helpful if you want to change some attributes + /// of the sound instance before you play it. For example, you could + /// call [setRelativePlaySpeed] or [setProtectVoice] on the sound before + /// un-pausing it. + /// + /// To play a looping sound, set [paused] to `true`. You can also + /// define the region to loop by setting [loopingStartAt] + /// (which defaults to the beginning of the sound otherwise). + /// There is no way to set the end of the looping region — it will + /// always be the end of the [sound]. + /// + /// Returns the [SoundHandle] of the new sound instance. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. Future play( @@ -978,9 +1008,7 @@ interface class SoLoud { return ret.newHandle; } - /// Pause or unpause an already loaded sound identified by [handle]. - /// - /// [handle] the sound handle. + /// Pause or unpause a currently playing sound identified by [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void pauseSwitch(SoundHandle handle) { @@ -990,9 +1018,7 @@ interface class SoLoud { SoLoudController().soLoudFFI.pauseSwitch(handle); } - /// Pause or unpause an already loaded sound identified by [handle]. - /// - /// [handle] the sound handle. + /// Pause or unpause a currently playing sound identified by [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setPause(SoundHandle handle, bool pause) { @@ -1002,9 +1028,7 @@ interface class SoLoud { SoLoudController().soLoudFFI.setPause(handle, pause ? 1 : 0); } - /// Gets the pause state of an already loaded sound identified by [handle]. - /// - /// [handle] the sound handle. + /// Gets the pause state of a currently playing sound identified by [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. bool getPause(SoundHandle handle) { @@ -1015,18 +1039,20 @@ interface class SoLoud { } /// 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. /// + /// Provide the currently playing sound instance via its [handle], + /// and the new [speed]. + /// + /// Setting the speed value to `0` will cause undefined behavior, + /// likely a crash. + /// + /// This changes the effective sample rate + /// while leaving the base sample rate alone. /// Note that playing a sound at a higher sample rate will require SoLoud /// to request more samples from the sound source, which will require more /// memory and more processing power. Playing at a slower sample /// rate is cheaper. /// - /// [handle] the sound handle. - /// [speed] the new speed. - /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setRelativePlaySpeed(SoundHandle handle, double speed) { if (!isInitialized) { @@ -1035,9 +1061,8 @@ interface class SoLoud { SoLoudController().soLoudFFI.setRelativePlaySpeed(handle, speed); } - /// Get a sound's relative play speed. - /// - /// [handle] the sound handle. + /// Get a sound's relative play speed. Provide the sound instance via + /// its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. double getRelativePlaySpeed(SoundHandle handle) { @@ -1047,10 +1072,10 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getRelativePlaySpeed(handle); } - /// Stop already loaded sound identified by [handle] and clear it from the - /// sound handle list. + /// Stop a currently playing sound identified by [handle] + /// and clear it from the sound handle list. /// - /// [handle] the sound handle to stop. + /// This does _not_ dispose the audio source. Use [disposeSource] for that. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. Future stop(SoundHandle handle) async { @@ -1074,40 +1099,50 @@ interface class SoLoud { } } - /// Stop all handles of the already loaded sound identified - /// by soundHash of [sound] and dispose it. + /// A deprecated alias of [disposeSource]. + @Deprecated("Use 'disposeSource' instead") + Future disposeSound(AudioSource sound) => disposeSource(sound); + + /// Stops all handles of the already loaded [source], and reclaims memory. /// - /// [sound] the sound to clear. + /// After an audio source has been disposed in this way, + /// do not attempt to play it. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. - Future disposeSound(AudioSource sound) async { + Future disposeSource(AudioSource source) async { if (!isInitialized) { throw const SoLoudNotInitializedException(); } _mainToIsolateStream?.send( { 'event': MessageEvents.disposeSound, - 'args': (soundHash: sound.soundHash), + 'args': (soundHash: source.soundHash), }, ); await _waitForEvent( - MessageEvents.disposeSound, (soundHash: sound.soundHash)); + MessageEvents.disposeSound, (soundHash: source.soundHash)); - await sound.soundEventsController.close(); + await source.soundEventsController.close(); /// remove the sound with [soundHash] _activeSounds.removeWhere( (element) { - return element.soundHash == sound.soundHash; + return element.soundHash == source.soundHash; }, ); } - /// Disposes all sounds already loaded. Complete silence. + /// A deprecated alias to [disposeAllSources]. + @Deprecated("Use 'disposeAllSources()' instead") + Future disposeAllSound() => disposeAllSources(); + + /// Disposes all audio sources that are currently loaded. + /// Also stops all sound instances if anything is playing. /// /// No need to call this method when shutting down the engine. + /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. - Future disposeAllSound() async { + Future disposeAllSources() async { if (!_isEngineInitialized) { throw const SoLoudNotInitializedException(); } @@ -1123,10 +1158,9 @@ interface class SoLoud { _activeSounds.clear(); } - /// Query whether a sound is set to loop. + /// Query whether a sound (supplied via [handle]) is set to loop. /// - /// [handle] the sound handle. - /// Returns true if flagged for looping. + /// Returns `true` if the sound is flagged for looping. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. bool getLooping(SoundHandle handle) { @@ -1136,11 +1170,8 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getLooping(handle); } - /// This function can be used to set a sample to play on repeat, - /// instead of just playing once. - /// - /// [handle] the handle for which enable or disable the loop - /// [enable] whether to enable looping or not. + /// Set the looping flag of a currently playing sound, provided via + /// its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setLooping(SoundHandle handle, bool enable) { @@ -1150,10 +1181,10 @@ interface class SoLoud { SoLoudController().soLoudFFI.setLooping(handle, enable); } - /// Get sound loop point value. + /// Get the loop point value of a currently playing sound, provided via + /// its [handle]. /// - /// [handle] the sound handle. - /// Returns the time in seconds. + /// Returns the timestamp of the loop point as a [Duration]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. Duration getLoopPoint(SoundHandle handle) { @@ -1163,10 +1194,10 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getLoopPoint(handle); } - /// Set sound loop point value. + /// Set the loop point of a currently playing sound, provided via + /// its [handle]. /// - /// [handle] the sound handle. - /// [time] in seconds. + /// Specify the loop point with [time] (a [Duration]). /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setLoopPoint(SoundHandle handle, Duration time) { @@ -1177,9 +1208,10 @@ interface class SoLoud { } /// Enable or disable visualization. + /// /// When enabled it will be possible to get FFT and wave data. /// - /// [enabled] wheter to set the visualization or not. + /// [enabled] whether to set the visualization or not. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setVisualizationEnabled(bool enabled) { @@ -1202,36 +1234,39 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getVisualizationEnabled(); } - /// Get the sound length in seconds. + /// Get the length of a loaded audio [source]. /// - /// [sound] the sound hash to get the length. + /// Returns the length as a [Duration]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. - Duration getLength(AudioSource sound) { + Duration getLength(AudioSource source) { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.getLength(sound.soundHash); + return SoLoudController().soLoudFFI.getLength(source.soundHash); } - /// Seek playing in seconds. + /// Seek a currently playing sound instance, provided via its [handle]. + /// Specify the [time] (as a [Duration]) to which you want to + /// move the play head. /// - /// [time] the time to seek. - /// [handle] the sound handle. + /// For example, seeking to `Duration(milliseconds: 200)` means that + /// you want to move the play head to a point 200 milliseconds into + /// the audio source. Seeking to [Duration.zero] means "go to the beginning + /// of the sound". /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. /// - /// NOTE: when seeking an MP3 file loaded using `mode`=`LoadMode.disk` the - /// seek operation is performed but there will be delays. This occurs because + /// NOTE: when seeking an MP3 file loaded using [LoadMode.disk], the + /// seek operation is performed but there will be a delay. 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. + /// The problem is explained in `souloud_wavstream.cpp`, + /// in the `WavStreamInstance::seek` function. + /// Therefore, [LoadMode.disk] is useful for things like the background music, + /// and not for things like a music player where the user + /// expects being able to seek anywhere inside a playing track immediately. /// If you need to seek MP3s without lags, please, use - /// `mode`=`LoadMode.memory` instead or other supported audio formats! - /// + /// [LoadMode.memory] instead, or use other supported audio formats. void seek(SoundHandle handle, Duration time) { if (!isInitialized) { throw const SoLoudNotInitializedException(); @@ -1244,10 +1279,12 @@ interface class SoLoud { } } - /// Get current sound position in seconds. + /// Get the current sound position of a sound instance (provided via its + /// [handle]). /// - /// [handle] the sound handle. - /// Return the position in seconds. + /// Returns the position as a [Duration]. For example, + /// `Duration(milliseconds: 200)` means that the play head is currently + /// 200 milliseconds into the audio source. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. Duration getPosition(SoundHandle handle) { @@ -1257,9 +1294,10 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getPosition(handle); } - /// Get current global volume. + /// Gets the current global volume. /// - /// Return the volume. + /// Return the volume as a [double], with `0.0` meaning silence + /// and `1.0` meaning full volume. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. double getGlobalVolume() { @@ -1269,7 +1307,10 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getGlobalVolume(); } - /// Set global volume for all the sounds. + /// Sets the global volume which affects all sounds. + /// + /// The value of [volume] can range from `0.0` (meaning everything is muted) + /// to `1.0` (meaning full volume). /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setGlobalVolume(double volume) { @@ -1284,9 +1325,11 @@ interface class SoLoud { } } - /// Get current [handle] volume. + /// Get the volume of the currently playing sound instance, provided + /// via its [handle]. /// - /// Return the volume. + /// Returns the volume as a [double], where `0.0` means the sound is muted, + /// and `1.0` means its playing at full volume. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. double getVolume(SoundHandle handle) { @@ -1296,10 +1339,11 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getVolume(handle); } - /// Set the volume for the given [handle]. + /// Set the volume for a currently playing sound instance, provided + /// via its [handle]. /// - /// [handle] the sound handle. - /// [volume] the new volume to set. + /// The value of [volume] can range from `0.0` (meaning the sound is muted) + /// to `1.0` (meaning it should play at full volume). /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void setVolume(SoundHandle handle, double volume) { @@ -1314,10 +1358,11 @@ interface class SoLoud { } } - /// Check if a handle is still valid. + /// Check if the [handle] is still valid. /// - /// [handle] handle to check. - /// Return true if valid. + /// Returns `true` if the sound instance identified by its [handle] is + /// currently playing or paused. Returns `false` if it's been stopped + /// or if it finished playing. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. bool getIsValidVoiceHandle(SoundHandle handle) { @@ -1337,11 +1382,11 @@ interface class SoLoud { /// Returns the number of concurrent sounds that are playing a /// specific audio source. - int countAudioSource(SoundHash soundHash) { + int countAudioSource(AudioSource audioSource) { if (!isInitialized) { throw const SoLoudNotInitializedException(); } - return SoLoudController().soLoudFFI.countAudioSource(soundHash); + return SoLoudController().soLoudFFI.countAudioSource(audioSource.soundHash); } /// Returns the number of voices the application has told SoLoud to play. @@ -1353,6 +1398,8 @@ interface class SoLoud { } /// Get a sound's protection state. + /// + /// See [setProtectVoice] for details] bool getProtectVoice(SoundHandle handle) { if (!isInitialized) { throw const SoLoudNotInitializedException(); @@ -1360,21 +1407,29 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getProtectVoice(handle); } - /// Set a sound's protection state. + /// Sets a sound instance's protection state. /// - /// Normally, if you try to play more sounds than there are voices, + /// The sound is specified via its [handle]. + /// + /// Normally, if you try to play more sounds than there are voices + /// (a.k.a. "channels"), /// 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. + /// This is normally okay _except_ when you have background music + /// or ambience playing. + /// These sounds will likely be the oldest playing sounds, and you don't + /// want them to be stopped just because there's a lot of sound effects + /// playing at the same time. + /// + /// You can solve this by protecting the sound instance. + /// Normally, you'd want to call [setProtectVoice] on all long-running, + /// looping or somehow especially important audio. + /// + /// If all voices are protected, the result is undefined. /// The number of protected entries is inclusive in the - /// max number active voice count [getMaxActiveVoiceCount]. - /// For example when having 16 max active voice count set to 16, and + /// maximum number of active voices [getMaxActiveVoiceCount]. + /// For example, when having max active voice count set to 16, and /// you want to play 20 other sounds, the protected voice will still play /// but you will hear only 15 of the other 20. - /// - /// [handle] handle to check. - /// [protect] whether to protect or not. void setProtectVoice(SoundHandle handle, bool protect) { if (!isInitialized) { throw const SoLoudNotInitializedException(); @@ -1382,7 +1437,7 @@ interface class SoLoud { SoLoudController().soLoudFFI.setProtectVoice(handle, protect); } - /// Get the current maximum active voice count. + /// Gets the current maximum active voice count. int getMaxActiveVoiceCount() { if (!isInitialized) { throw const SoLoudNotInitializedException(); @@ -1390,18 +1445,20 @@ interface class SoLoud { return SoLoudController().soLoudFFI.getMaxActiveVoiceCount(); } - /// Set the current maximum active voice count. + /// Sets 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 + /// voices would cause performance issues, and could lead unnecessary + /// clipping. The default number of maximum concurrent voices is 16, + /// but this can be adjusted at runtime. + /// + /// The hard maximum count 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. + /// need more than 4095 sounds playing _at once_, + /// you're probably going to need some serious changes anyway. void setMaxActiveVoiceCount(int maxVoiceCount) { if (!isInitialized) { throw const SoLoudNotInitializedException(); @@ -1471,10 +1528,8 @@ interface class SoLoud { // faders // ////////////////////////////////////// - /// Smoothly change the global volume over specified time. - /// - /// [to] the volume to fade to. - /// [time] the time in seconds to change the volume. + /// Smoothly changes the global volume to the value of [to] + /// over specified [time]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void fadeGlobalVolume(double to, Duration time) { @@ -1489,11 +1544,10 @@ interface class SoLoud { } } - /// Smoothly change a channel's volume over specified time. + /// Smoothly changes a single sound instance's volume + /// to the value of [to] over the specified [time]. /// - /// [handle] the sound handle. - /// [to] the volume to fade to. - /// [time] the time in seconds to change the volume. + /// The sound instance is provided via its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void fadeVolume(SoundHandle handle, double to, Duration time) { @@ -1508,11 +1562,10 @@ interface class SoLoud { } } - /// Smoothly change a channel's pan setting over specified time. + /// Smoothly changes a currently playing sound's pan setting + /// to the value of [to] over specified [time]. /// - /// [handle] the sound handle. - /// [to] the pan value to fade to. - /// [time] the time in seconds to change the pan. + /// The sound instance is provided via its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void fadePan(SoundHandle handle, double to, Duration time) { @@ -1527,11 +1580,10 @@ interface class SoLoud { } } - /// Smoothly change a channel's relative play speed over specified time. + /// Smoothly changes a currently playing sound's relative play speed + /// to the value of [to] over specified [time]. /// - /// [handle] the sound handle. - /// [to] the speed value to fade to. - /// [time] the time in seconds to change the speed. + /// The sound instance is provided via its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void fadeRelativePlaySpeed(SoundHandle handle, double to, Duration time) { @@ -1547,10 +1599,9 @@ interface class SoLoud { } } - /// After specified time, pause the channel. + /// Waits the specified [time], then pauses the currently playing sound. /// - /// [handle] the sound handle. - /// [time] the time in seconds to pause. + /// The sound instance is provided via its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void schedulePause(SoundHandle handle, Duration time) { @@ -1565,10 +1616,9 @@ interface class SoLoud { } } - /// After specified time, stop the channel. + /// Waits the specified [time], then stops the currently playing sound. /// - /// [handle] the sound handle. - /// [time] the time in seconds to pause. + /// The sound instance is provided via its [handle]. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void scheduleStop(SoundHandle handle, Duration time) { @@ -1583,12 +1633,13 @@ interface class SoLoud { } } - /// Set fader to oscillate the volume at specified frequency. + /// Sets fader to oscillate the volume at specified frequency. + /// + /// The sound instance is specified via its [handle]. /// - /// [handle] the sound handle. - /// [from] the lowest value for the oscillation. - /// [to] the highest value for the oscillation. - /// [time] the time in seconds to oscillate. + /// The value of [from] is the lowest value for the oscillation. + /// The value of [to] is the highest value for the oscillation. + /// The specified [time] is the period of oscillation. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void oscillateVolume( @@ -1605,12 +1656,13 @@ interface class SoLoud { } } - /// Set fader to oscillate the panning at specified frequency. + /// Sets oscillation of the pan at specified frequency. /// - /// [handle] the sound handle. - /// [from] the lowest value for the oscillation. - /// [to] the highest value for the oscillation. - /// [time] the time in seconds to oscillate. + /// The sound instance is specified via its [handle]. + /// + /// The value of [from] is the leftmost value for the oscillation. + /// The value of [to] is the rightmost value for the oscillation. + /// The specified [time] is the period of oscillation. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void oscillatePan(SoundHandle handle, double from, double to, Duration time) { @@ -1626,12 +1678,13 @@ interface class SoLoud { } } - /// Set fader to oscillate the relative play speed at specified frequency. + /// Sets oscillation of the play speed at specified frequency. + /// + /// The sound instance is specified via its [handle]. /// - /// [handle] the sound handle. - /// [from] the lowest value for the oscillation. - /// [to] the highest value for the oscillation. - /// [time] the time in seconds to oscillate. + /// The value of [from] is the lowest value for the oscillation. + /// The value of [to] is the highest value for the oscillation. + /// The specified [time] is the period of oscillation. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void oscillateRelativePlaySpeed( @@ -1651,9 +1704,9 @@ interface class SoLoud { /// Set fader to oscillate the global volume at specified frequency. /// - /// [from] the lowest value for the oscillation. - /// [to] the highest value for the oscillation. - /// [time] the time in seconds to oscillate. + /// The value of [from] is the lowest value for the oscillation. + /// The value of [to] is the highest value for the oscillation. + /// The specified [time] is the period of oscillation. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. void oscillateGlobalVolume(double from, double to, Duration time) { @@ -1761,11 +1814,10 @@ interface class SoLoud { // / Filters // /////////////////////////////////////// - /// Check if the given filter is active or not. + /// Checks whether the given [filterType] is active. /// - /// [filterType] filter to check - /// Returns [PlayerErrors.noError] if no errors and the index of - /// the given filter (-1 if the filter is not active) + /// 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); if (ret.error != PlayerErrors.noError) { @@ -1777,10 +1829,9 @@ interface class SoLoud { // TODO(marco): add a method to rearrange filters order? - /// Get parameters names of the given filter. + /// Gets parameters of the given [filterType]. /// - /// [filterType] filter to get param names - /// Returns the list of param names + /// Returns the list of param names. List getFilterParamNames(FilterType filterType) { final ret = SoLoudController().soLoudFFI.getFilterParamNames(filterType.index); @@ -1791,9 +1842,7 @@ interface class SoLoud { return ret.names; } - /// Add a filter to all sounds. - /// - /// [filterType] filter to add. + /// Adds a [filterType] to all sounds. /// /// Throws [SoLoudMaxFilterNumberReachedException] when the max number of /// concurrent filter is reached (default max filter is 8). @@ -1807,9 +1856,7 @@ interface class SoLoud { } } - /// Remove filter from all sounds. - /// - /// [filterType] filter to remove. + /// Removes [filterType] from all sounds. void removeGlobalFilter(FilterType filterType) { final ret = SoLoudController().soLoudFFI.removeGlobalFilter(filterType.index); @@ -1825,12 +1872,10 @@ interface class SoLoud { void setFxParams(FilterType filterType, int attributeId, double value) => setFilterParameter(filterType, attributeId, value); - /// Set the effect parameter. - /// - /// [filterType] the filter to change the parameter to. - /// [attributeId] the attribute ID to change. - /// [value] the new value. + /// Sets a parameter of the given [filterType]. /// + /// Specify the [attributeId] of the parameter (which you can learn from + /// [getFilterParamNames]), and its new [value]. void setFilterParameter( FilterType filterType, int attributeId, double value) { final ret = SoLoudController() @@ -1848,12 +1893,12 @@ interface class SoLoud { double getFxParams(FilterType filterType, int attributeId) => getFilterParameter(filterType, attributeId); - /// Get the effect current parameter. + /// Gets the value of a parameter of the given [filterType]. /// - /// [filterType] the filter to query the parameter. - /// [attributeId] the ID of the attribute to request the value from. - /// Returns the value of param + /// Specify the [attributeId] of the parameter (which you can learn from + /// [getFilterParamNames]). /// + /// Returns the value as [double]. double getFilterParameter(FilterType filterType, int attributeId) { return SoLoudController() .soLoudFFI @@ -1863,26 +1908,36 @@ interface class SoLoud { // //////////////////////////////////////////////// // Below all the methods implemented with FFI for the 3D audio // more info: https://solhsa.com/soloud/core3d.html - // - // coordinate system is right handed - // Y - // ^ - // | - // | - // | - // --------> X - // / - // / - // Z // //////////////////////////////////////////////// - /// play3d() is the 3d version of the play() call. - /// The listener position is (0, 0, 0) by default. + /// This function is the 3D version of the [play] call. + /// + /// The coordinate system is right handed. + /// + /// ```text + /// Y + /// ^ + /// | + /// | + /// | + /// --------> X + /// / + /// / + /// Z + /// ``` + /// + /// The listener position is `(0, 0, 0)` by default but can be changed + /// with [set3dListenerParameters]. + /// + /// The parameters [posX], [posY] and [posZ] are the audio source's + /// position coordinates. + /// + /// The parameters [velX], [velY] and [velZ] are the audio source's velocity. + /// Defaults to `(0, 0, 0)`. + /// + /// The rest of the parameters are equivalent to the non-3D version of this + /// method ([play]). /// - /// [posX], [posY], [posZ] are the audio source position coordinates. - /// [velX], [velY], [velZ] are the audio source velocity. - /// Defaults to (0, 0, 0). - /// [volume] the playing volume. Default to 1. /// Returns the [SoundHandle] of this new sound. /// /// Throws [SoLoudNotInitializedException] if the engine is not initialized. @@ -1966,13 +2021,15 @@ interface class SoLoud { SoLoudController().soLoudFFI.set3dSoundSpeed(speed); } - /// Get the sound speed. + /// Gets the speed of sound. + /// + /// See [set3dSoundSpeed] for details. double get3dSoundSpeed() { return SoLoudController().soLoudFFI.get3dSoundSpeed(); } - /// You can set the position, at-vector, up-vector and velocity - /// parameters of the 3d audio listener with one call. + /// Sets the position, at-vector, up-vector and velocity + /// parameters of the 3D audio listener with one call. void set3dListenerParameters( double posX, double posY, @@ -1990,22 +2047,22 @@ interface class SoLoud { atY, atZ, upX, upY, upZ, velocityX, velocityY, velocityZ); } - /// You can set the position parameter of the 3d audio listener. + /// Sets the position parameter of the 3D audio listener. void set3dListenerPosition(double posX, double posY, double posZ) { SoLoudController().soLoudFFI.set3dListenerPosition(posX, posY, posZ); } - /// You can set the "at" vector parameter of the 3d audio listener. + /// 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); } - /// You can set the "up" vector parameter of the 3d audio listener. + /// Sets the up-vector parameter of the 3D audio listener. void set3dListenerUp(double upX, double upY, double upZ) { SoLoudController().soLoudFFI.set3dListenerUp(upX, upY, upZ); } - /// You can set the listener's velocity vector parameter. + /// Sets the 3D listener's velocity vector. void set3dListenerVelocity( double velocityX, double velocityY, double velocityZ) { SoLoudController() @@ -2013,21 +2070,23 @@ interface class SoLoud { .set3dListenerVelocity(velocityX, velocityY, velocityZ); } - /// You can set the position and velocity parameters of a live - /// 3d audio source with one call. + /// Sets the position and velocity parameters of a live + /// 3D audio source with one call. + /// + /// 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( handle, posX, posY, posZ, velocityX, velocityY, velocityZ); } - /// You can set the position parameters of a live 3d audio source. + /// 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); } - /// You can set the velocity parameters of a live 3d audio source. + /// Set the velocity parameter of a live 3D audio source. void set3dSourceVelocity(SoundHandle handle, double velocityX, double velocityY, double velocityZ) { SoLoudController() @@ -2035,8 +2094,8 @@ interface class SoLoud { .set3dSourceVelocity(handle, velocityX, velocityY, velocityZ); } - /// You can set the minimum and maximum distance parameters - /// of a live 3d audio source. + /// Sets the minimum and maximum distance parameters + /// of a live 3D audio source. void set3dSourceMinMaxDistance( SoundHandle handle, double minDistance, double maxDistance) { SoLoudController() @@ -2045,15 +2104,16 @@ interface class SoLoud { } /// You can change the attenuation model and rolloff factor parameters of - /// a live 3d audio source. + /// a live 3D audio source. + /// /// ``` /// 0 NO_ATTENUATION No attenuation /// 1 INVERSE_DISTANCE Inverse distance attenuation model /// 2 LINEAR_DISTANCE Linear distance attenuation model /// 3 EXPONENTIAL_DISTANCE Exponential distance attenuation model /// ``` - /// see https://solhsa.com/soloud/concepts3d.html /// + /// See https://solhsa.com/soloud/concepts3d.html. void set3dSourceAttenuation( SoundHandle handle, int attenuationModel, @@ -2066,7 +2126,7 @@ interface class SoLoud { ); } - /// You can change the doppler factor of a live 3d audio source. + /// Sets the doppler factor of a live 3D audio source. void set3dSourceDopplerFactor(SoundHandle handle, double dopplerFactor) { SoLoudController() .soLoudFFI diff --git a/lib/src/utils/assets_manager.dart b/lib/src/utils/assets_manager.dart index 2bcf752..1564599 100644 --- a/lib/src/utils/assets_manager.dart +++ b/lib/src/utils/assets_manager.dart @@ -9,6 +9,7 @@ import 'package:path_provider/path_provider.dart'; /// The [AssetsManager] class provides a static method to retrieve an asset /// file and save it to the local file system. /// +@Deprecated('Use SoLoud.loadAsset instead') class AssetsManager { static final Logger _log = Logger('flutter_soloud.AssetsManager'); diff --git a/pubspec.yaml b/pubspec.yaml index ed5be81..dd5312d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,8 @@ name: flutter_soloud description: >- - Flutter audio plugin using SoLoud library with miniaudio backend and FFI. - It provides player, basic capture from microphone, 3D audio and more. + A low-level audio plugin for Flutter, + mainly meant for games and immersive apps. + Based on the SoLoud (C++) audio engine. version: 2.0.0-pre.4 issue_tracker: https://github.com/alnitak/flutter_soloud/issues homepage: https://github.com/alnitak/flutter_soloud @@ -26,33 +27,21 @@ environment: flutter: '>=3.3.0' dependencies: + ffi: ^2.0.2 flutter: sdk: flutter - - #https://pub.dev/packages/ffi - ffi: ^2.0.2 - #https://pub.dev/packages/http http: ^1.1.0 - #https://pub.dev/packages/logging logging: ^1.0.0 - #https://pub.dev/packages/meta meta: ^1.0.0 - #https://pub.dev/packages/path path: ^1.8.1 - #https://pub.dev/packages/path_provider path_provider: ^2.0.15 - plugin_platform_interface: ^2.1.5 dev_dependencies: - #https://pub.dev/packages/ffigen ffigen: ^9.0.1 - flutter_test: sdk: flutter - test: ^1.24.9 - very_good_analysis: ^5.1.0 flutter: @@ -68,5 +57,3 @@ flutter: ffiPlugin: true windows: ffiPlugin: true - - diff --git a/test_fixes/dispose_source.dart b/test_fixes/dispose_source.dart new file mode 100644 index 0000000..2a1a00d --- /dev/null +++ b/test_fixes/dispose_source.dart @@ -0,0 +1,7 @@ +import 'package:flutter_soloud/flutter_soloud.dart'; + +Future main() async { + final sound = AudioSource(SoundHash.invalid()); + SoLoud.instance.disposeSound(sound); + SoLoud.instance.disposeAllSound(); +} diff --git a/test_fixes/dispose_source.dart.expect b/test_fixes/dispose_source.dart.expect new file mode 100644 index 0000000..9a62698 --- /dev/null +++ b/test_fixes/dispose_source.dart.expect @@ -0,0 +1,7 @@ +import 'package:flutter_soloud/flutter_soloud.dart'; + +Future main() async { + final sound = AudioSource(SoundHash.invalid()); + SoLoud.instance.disposeSource(sound); + SoLoud.instance.disposeAllSources(); +} From 345aa572c86de4d2adc4d0d8b6ef76d7a82219ee Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Wed, 3 Apr 2024 20:37:22 +0200 Subject: [PATCH 2/3] Fix bad example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 013d1fa..39d53a0 100755 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ void example() async { // ... await soloud.stop(handle); - await soloud.disposeSound(source); + await soloud.disposeSource(source); } ``` From c067eb8287754ed7af268cc9a5fe55eed997a5aa Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Thu, 4 Apr 2024 09:59:51 +0200 Subject: [PATCH 3/3] Add AUTHORS file --- AUTHORS | 11 +++++++++++ LICENSE | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 AUTHORS diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 0000000..627c768 --- /dev/null +++ b/AUTHORS @@ -0,0 +1,11 @@ +# This is the list of flutter_soloud's significant contributors. +# +# This does not necessarily list everyone who has contributed code, +# especially since many employees of one corporation may be contributing. +# To see the full list of contributors, see the revision history in +# source control. + +Marco Bavagnoli +Filip Hracek +mklimko-perpetio +Jemis Goti diff --git a/LICENSE b/LICENSE index 1ee8fec..30e9a29 100755 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2024 Marco Bavagnoli +Copyright (c) 2024 The flutter_soloud Authors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal