├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── asconfig.json ├── demo ├── .vscode │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── README.md ├── application.xml ├── asconfig.json ├── cert │ └── syrinx-demo.p12 ├── libs │ ├── AeonDesktopTheme.swc │ ├── MetalWorksDesktopTheme.swc │ ├── MinimalDesktopTheme.swc │ ├── feathers.swc │ └── starling.swc ├── media │ ├── images │ │ └── Syrinx-Sound-Manager-Demo.png │ └── sounds │ │ ├── piano_IEEE32_mono_44100.wav │ │ ├── piano_IEEE32_stereo_44100.wav │ │ ├── piano_MP3_mono_44100.mp3 │ │ ├── piano_MP3_stereo_44100.mp3 │ │ ├── piano_PCM16_mono_44100.wav │ │ └── piano_PCM16_stereo_44100.wav └── src │ └── ch │ └── adolio │ ├── Main.as │ ├── StarlingMain.as │ └── sound │ ├── SoundInstanceEntry.as │ ├── SoundInstanceEntryHeader.as │ ├── SoundManagerTest.as │ ├── TrackConfigurationEntry.as │ └── TrackConfigurationEntryHeader.as ├── libs └── as3-signals.swc └── src └── ch └── adolio └── sound ├── Mp3Track.as ├── SoundInstance.as ├── SoundInstancesPool.as ├── SoundManager.as ├── Track.as ├── TrackConfiguration.as └── WavTrack.as /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | demo/bin/ 4 | demo/obj/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "as3mxml.sdk.framework": "c:\\AIR\\AIR_SDK_33_1", 3 | "as3mxml.sources.organizeImports.insertNewLineBetweenTopLevelPackages": false, 4 | "files.trimTrailingWhitespace": true 5 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "actionscript", 8 | "debug": false 9 | }, 10 | { 11 | "type": "actionscript", 12 | "debug": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Syrinx - Sound Manager: Changelog 2 | 3 | ## v0.6 (2022-11-12) 4 | 5 | - Sound Manager: Added `Sound Instance` pooling capability 6 | - Sound Manager: Fixed typo in `getRegisteredTracks` method 7 | - Demo: Updated to support the new pooling capability 8 | - Demo: Fixed viewport 9 | - Demo: Improved events handling 10 | 11 | ## v0.5 (2022-07-26) 12 | 13 | - Track Configuration: Added base volume for volume mastering configuration 14 | - Sound Instance: Added base volume for volume mastering 15 | - Sound Manager: Added `findRegisteredTrack` method 16 | - Sound Instance: Fixed minor constant usage 17 | 18 | ## v0.4 (2022-01-11) 19 | 20 | - Sound Instance: Fixed looping capability for standard sounds 21 | - Sound Instance: Added `currentLoop` accessor 22 | - Demo: Extended loops limit 23 | - Demo: Improved stop & re-play capability 24 | 25 | ## v0.3 (2020-09-30) 26 | 27 | - Sound Instance: Added remaining time method 28 | - Sound Manager: Fixed destroy all sound instances method 29 | - Track Configuration: Fixed missing parameter type 30 | 31 | ## v0.2 (2019-04-16) 32 | 33 | - Sound Instance: Improved performance for native MP3 sounds 34 | - Sound Instance: Added safe channel acquisition option 35 | - Sound Manager: Added max channel capacity capability 36 | - Sound Manager: Added getter for playing sound instances count 37 | - Sound Manager: Added optional automatic trimming durations detection at track registration 38 | - Demo: Fixed WAV file loading cancellation 39 | - Demo: Updated to show actually playing sound instances over registered sound instances 40 | 41 | ## v0.1 (2019-04-05) 42 | 43 | - Initial version of the library 44 | - Added WAV format support (PCM 16bit & IEEE 32bit, mono/stereo, 44100 Hz) 45 | - Added full sound management capability (see feature list in README.md) 46 | - Added custom sampling capability 47 | - Added trimming capability 48 | - Added pitch capability -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Simplified BSD License 2 | ====================== 3 | 4 | Copyright 2019-2022 Aurélien Da Campo (Adolio). All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without modification, 7 | are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this list of 10 | conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, this list 13 | of conditions and the following disclaimer in the documentation and/or other materials 14 | provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY ADOLIO "AS IS" AND ANY EXPRESS OR IMPLIED 17 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ADOLIO OR 19 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 20 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 22 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 23 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 24 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | The views and conclusions contained in the software and documentation are those of the 27 | authors and should not be interpreted as representing official policies, either expressed 28 | or implied, of Adolio. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Syrinx - Sound Manager for Adobe AIR 2 | 3 | by Aurélien Da Campo ([Adolio](https://twitter.com/AurelienDaCampo)) 4 | 5 | ## ⭐ Key features 6 | 7 | - MP3 & WAV formats support 8 | - Tracks registration / configuration management 9 | - Intuitive & friendly API 10 | - Seamless sound looping via silence trimming and/or WAV files format 11 | - Pitch capability 12 | - Sound instances pooling 13 | 14 | ## ▶️ Try it! 15 | 16 | Go to the [demo](./demo/) folder, configure the project & run it or if you get the latest [release](https://github.com/Adolio/AS3-Sound-Manager/releases), the demo binary should be available in the archive. 17 | 18 | ## 📄 Full features 19 | 20 | - **Sound Manager** 21 | - Tracks registration management 22 | - Register / unregister track 23 | - Get all registered tracks 24 | - Has registered track? 25 | - **Track configuration** 26 | - Type 27 | - Base volume (for mastering) 28 | - Sampling rate 29 | - Trimming (start / end) 30 | - Automatic trimming detection 31 | - Sound instantiation from registered tracks 32 | - Play sound (returns a *Sound Instance*) 33 | - Starting volume 34 | - Starting position 35 | - Loop & infinite looping 36 | - Starting paused 37 | - Sound format modularity 38 | - Supported formats: 39 | - MP3 40 | - WAV 41 | - Sound instances management 42 | - Master & base volume 43 | - Get all sound instances 44 | - Get all sound instances by type 45 | - Stop all sound instances 46 | - Destroy all sound instances 47 | - Max channel capacity 48 | - Events 49 | - Track registered 50 | - Track unregistered 51 | - Sound instance added 52 | - Sound instance removed 53 | - **Sound Instance** 54 | - Controls 55 | - Execution (play / pause / resume / stop) 56 | - Volume 57 | - Position (time & ratio) 58 | - Mute 59 | - Pan 60 | - Pitch 61 | - Status & configuration 62 | - Length 63 | - Total length (incl. loops) 64 | - Volume / Mixed volume 65 | - Is muted? 66 | - Pan 67 | - Pitch 68 | - Loops / remaining loops 69 | - Position (time & ratio) 70 | - Remaining time 71 | - Is playing? 72 | - Is started? 73 | - Is paused? 74 | - Is destroyed? 75 | - Events 76 | - Started 77 | - Paused 78 | - Resumed 79 | - Stopped 80 | - Completed 81 | - Destroyed 82 | 83 | ## ⌨️ How to use? 84 | 85 | ### 📻 Sound Manager 86 | 87 | Main class to register tracks & instantiate sounds. The following example registers two tracks (MP3 & Wav), setups the trimming & sampling options. 88 | Then three sounds are instantiated. 89 | 90 | ```actionscript 91 | // Embedded sounds (Note that WAV files requires a mime type) 92 | [Embed(source = "../media/sound/engine.mp3")] public static const EngineSoundMp3:Class; 93 | [Embed(source = "../media/sound/engine.wav", mimeType="application/octet-stream")] public static const EngineSoundWav:Class; 94 | 95 | private var _soundManager:SoundManager; 96 | 97 | public function SoundManagerExample() 98 | { 99 | // Create Sound Manager 100 | _soundManager = new SoundManager(); 101 | _soundManager.maxChannelCapacity = 8; // Limit number of simultaneous playing sounds (for this sound manager only) 102 | _soundManager.isPoolingEnabled = true; // Enable pooling to re-used already instantiated sounds and reduce GC impacts 103 | 104 | // Register sounds 105 | var engineSoundMp3Config:TrackConfiguration = _soundManager.registerTrack("Engine 1", new Mp3Track(new EngineSoundMp3())); // Register a MP3 track 106 | var engineSoundWavConfig:TrackConfiguration = _soundManager.registerTrack("Engine 2", new WavTrack(new EngineSoundWav())); // Register a WAV track 107 | 108 | // Trimming configuration 109 | engineSoundMp3Config.findTrim(0.01); // Find start & end trim durations to remove silences inherent to mp3 format (with a silent threshold of 0.01) 110 | 111 | // Sampling rate configuration 112 | engineSoundWavConfig.sampling = 4096; // This defines how much samples are read per sampling request. The value must be between 2048 (included) and 8192 (included). 113 | 114 | // Base volume configuration for mastering 115 | engineSoundMp3Config.baseVolume = 0.5; // Custom mastering at track configuration level, the sound can now be player at 1.0 but will still be influenced by this mastering. 116 | 117 | // Play sounds 118 | var engine1_once:SoundInstance = _soundManager.play("Engine 1", 1.0); // Play once 119 | var engine1_twice:SoundInstance = _soundManager.play("Engine 1", 1.0, 0, 1); // Play twice (repeat once) 120 | var engine2_infinite:SoundInstance = _soundManager.play("Engine 2", 1.0, 0, -1); // Play infinite 121 | 122 | // Control master volume 123 | _soundManager.volume = 0.8; 124 | } 125 | ``` 126 | 127 | ### 🎵 Sound Instance 128 | 129 | Instance of a sound that can be controlled. The following example shows how to listen to sound events, how to control a sound and how to get various information about it. 130 | 131 | ```actionscript 132 | var si:SoundInstance = _soundManager.play("Engine 1", 1.0, 1337, 5); // Play at max volume, starting at 1,337 sec., 6 times (looping 5 times) 133 | 134 | //----------------------------------------------------------------------------- 135 | //-- Events 136 | //----------------------------------------------------------------------------- 137 | 138 | si.started.add(function (si:SoundInstance):void { trace("Sound started. " + si.type); } ); 139 | si.stopped.add(function (si:SoundInstance):void { trace("Sound stopped. " + si.type); } ); 140 | si.paused.add(function (si:SoundInstance):void { trace("Sound paused. " + si.type); } ); 141 | si.resumed.add(function (si:SoundInstance):void { trace("Sound resumed. " + si.type); } ); 142 | si.completed.add(function (si:SoundInstance):void { trace("Sound completed. " + si.type); } ); 143 | si.destroyed.add(function (si:SoundInstance):void { trace("Sound destroyed. " + si.type); } ); 144 | 145 | //----------------------------------------------------------------------------- 146 | //-- Controls 147 | //----------------------------------------------------------------------------- 148 | 149 | // Execution controls 150 | si.pause(); 151 | si.resume(); 152 | si.stop(); 153 | si.play(0.5, 0, 1.0); // (Re)play twice (repeat once) 154 | 155 | // Position 156 | si.position = 1337; // In milliseconds 157 | si.positionRatio = 0.5; // This includes the loops so here it will play only the second loop! 158 | 159 | // Mute 160 | si.isMuted = true; 161 | si.isMuted = false; 162 | 163 | // Volume 164 | si.volume = 0; 165 | si.volume = 0.8; 166 | 167 | // Pan 168 | si.pan = -0.5; // Half left 169 | 170 | // Pitch 171 | si.pitch = 0.8; // 20% slower 172 | si.pitch = 1.2; // 20% faster 173 | si.pitch = 1.0; // Back to default 174 | 175 | //----------------------------------------------------------------------------- 176 | //-- Status info 177 | //----------------------------------------------------------------------------- 178 | trace("Type: " + si.type); // Type 179 | trace("Volume: " + si.volume); // Volume 180 | trace("Mixed volume: " + si.mixedVolume); // Equals to _soundManager.volume * volume 181 | trace("Pan: " + si.pan); // Pan 182 | trace("Pitch: " + si.pitch); // Pitch 183 | trace("Sound length: " + si.length); // One loop length in milliseconds (trimmed) 184 | trace("Sound total length: " + si.totalLength); // Total length in milliseconds (including loops & trim durations) 185 | trace("Loops: " + si.loops); // Total loops 186 | trace("Current loop: " + si.currentLoop); // Current loop (-1 = infinite, 0 = first play, 1 = second play, etc.) 187 | trace("Remaining loops: " + si.loopsRemaining); // Remaining loops 188 | trace("Remaining time: " + si.remainingTime); // Remaining time in milliseconds 189 | trace("Current position: " + si.position); // Current position in milliseconds 190 | trace("Current position (ratio): " + si.positionRatio); // Current position (ratio, between 0..1) 191 | trace("Is started: " + si.isStarted); // Is started? 192 | trace("Is playing: " + si.isPlaying); // Is playing? 193 | trace("Is paused: " + si.isPaused); // Is paused? 194 | trace("Is muted: " + si.isMuted); // Is muted? 195 | trace("Is destroyed: " + si.isDestroyed); // Is destroyed? 196 | ``` 197 | 198 | ### ♻️ Sound Instances Pooling 199 | 200 | Pooling can significantly reduce memory consumption by re-using already instantiated `Sound Instances`. This should also minimize lags caused by the `Garbage Collector` passage. Therefore this capability could be pretty useful when playing a lot of identical sounds in your application (e.g. player footsteps, arrows thrown by an army of archers, etc.). 201 | 202 | The pooling capability can be enabled this way: 203 | 204 | ```actionscript 205 | _soundManager.isPoolingEnabled = true; 206 | ``` 207 | 208 | **Important**: When a `Sound Instance` is completed, it will automatically be returned to the pool. Beware of not keeping references to completed `Sound Instances` because they might soon be re-used somewhere else. 209 | 210 | Infinite looping sounds (e.g. seamless looping sounds) can be manually returned to the pool by calling the following method: 211 | 212 | ```actionscript 213 | _soundManager.releaseSoundInstanceToPool(mySound); 214 | ``` 215 | 216 | **Important**: Never destroy `Sound Instances` when the pooling capability is active. This will break the objects recycling mechanism. 217 | 218 | ### 💡 Recommendations 219 | 220 | - Standard sounds (MP3 Tracks without trimming neither pitching options set) are treated as normal flash sound (no custom data sampling required) which provides better overall performance over advanced sounds processing. Sampling rate is therefore ignored for those sounds. 221 | - Wav file format is recommended for seamless looping sound since MP3 format introduces silent parts at the beginning & the end of the track. 222 | - Another option could be to use MP3 & configure trimming durations either manually or by using the `TrackConfiguration.findTrim()` utility method. 223 | - Use higher sampling rate for tracks that require more robustness against frame drop (e.g. during loading). 224 | 225 | ## 📦 How to install? 226 | 227 | - Checkout this repository & add `src` folder in your `classpath` or copy paste the content of the `src` folder in your source folder. 228 | 229 | or 230 | 231 | - Use the `.swc` file provided in each release. 232 | 233 | Don't forget to include dependencies (see below). 234 | 235 | ## ☝️ Limitations 236 | 237 | - Supported Wav formats: 238 | - PCM 16 bits, 1 channel, 44100 Hz 239 | - PCM 16 bits, 2 channels, 44100 Hz 240 | - IEEE Float 32 bits, 1 channel, 44100 Hz 241 | - IEEE Float 32 bits, 2 channels, 44100 Hz 242 | 243 | ## 🔗 Minimum Requirements 244 | 245 | - Adobe AIR 32.0 in order to fix crackling artifacts: https://github.com/Gamua/Adobe-Runtime-Support/issues/46 246 | 247 | ## 🖇 Dependencies (included in `libs` folder) 248 | 249 | - AS3 Signals: https://github.com/robertpenner/as3-signals -------------------------------------------------------------------------------- /asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "lib", 3 | "compilerOptions": { 4 | "source-path": [ 5 | "src" 6 | ], 7 | "external-library-path": [ 8 | "libs" 9 | ], 10 | "include-sources": [ 11 | "src" 12 | ], 13 | "output": "bin/syrinx.swc" 14 | } 15 | } -------------------------------------------------------------------------------- /demo/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "swf", 9 | "request": "launch", 10 | "name": "AIR desktop: Build release & launch", 11 | "profile": "extendedDesktop", 12 | "preLaunchTask": "ActionScript: compile release - asconfig.json" 13 | }, 14 | { 15 | "type": "swf", 16 | "request": "launch", 17 | "name": "AIR desktop: Build debug & launch", 18 | "profile": "extendedDesktop", 19 | "preLaunchTask": "ActionScript: compile debug - asconfig.json" 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "as3mxml.sdk.framework": "c:\\AIR\\AIR_SDK_33_1", 3 | "as3mxml.sources.organizeImports.insertNewLineBetweenTopLevelPackages": false, 4 | "files.trimTrailingWhitespace": true 5 | } -------------------------------------------------------------------------------- /demo/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "actionscript", 8 | "debug": false 9 | }, 10 | { 11 | "type": "actionscript", 12 | "debug": true 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Syrinx - Sound Manager - Demo 2 | 3 | by Aurélien Da Campo ([Adolio](https://twitter.com/AurelienDaCampo)) 4 | 5 | ## 📍 Introduction 6 | 7 | The demo shows the main features provided by the *Syrinx Sound Manager* extension for Adobe AIR. 8 | 9 | ![](media/images/Syrinx-Sound-Manager-Demo.png) 10 | 11 | This demo relies on [Starling Framework](https://github.com/Gamua/Starling-Framework) & [Feathers UI](https://github.com/BowlerHatLLC/feathers). 12 | 13 | ## 🎶 Resources origin 14 | 15 | ### Sounds 16 | - [Steinway cinematic phasing intro piano](https://freesound.org/people/XHALE303/sounds/440931/) by [XHALE303](https://freesound.org/people/XHALE303/) - This sound is licensed under the [Attribution Noncommercial License](https://creativecommons.org/licenses/by-nc/3.0/). 17 | 18 | ## 🔨 How to build? 19 | 20 | Install [Visual Studio Code](https://code.visualstudio.com/) and [ActionScript & MXML](https://as3mxml.com/#install-extension) and then follow the build procedure provided by the *ActionScript & MXML* extension. -------------------------------------------------------------------------------- /demo/application.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | Syrinx-Sound-Manager-Demo 4 | 0.6 5 | 0.6 6 | Syrinx-Sound-Manager-Demo 7 | Syrinx-Sound-Manager-Demo 8 | 9 | Syrinx-Sound-Manager-Demo 10 | Syrinx-Sound-Manager-Demo.swf 11 | standard 12 | false 13 | true 14 | true 15 | false 16 | false 17 | direct 18 | false 19 | 20 | desktop extendedDesktop 21 | -------------------------------------------------------------------------------- /demo/asconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": "air", 3 | "compilerOptions": { 4 | "output": "bin/Syrinx-Sound-Manager-Demo.swf", 5 | "library-path": [ 6 | "libs/starling.swc", 7 | "libs/feathers.swc", 8 | "libs/AeonDesktopTheme.swc", 9 | "libs/MinimalDesktopTheme.swc", 10 | "libs/MetalWorksDesktopTheme.swc", 11 | "../libs/as3-signals.swc" 12 | ], 13 | "source-path": [ 14 | "src", 15 | "../src/", 16 | "media" 17 | ], 18 | "default-size": { 19 | "width": 1000, 20 | "height": 800 21 | } 22 | }, 23 | "application": "application.xml", 24 | "files": [ 25 | "src/ch/adolio/Main.as" 26 | ], 27 | "airOptions": { 28 | "windows": { 29 | "target": "native", 30 | "output": "bin/Syrinx-Sound-Manager-Demo.exe", 31 | "signingOptions": { 32 | "storetype": "pkcs12", 33 | "keystore": "cert/syrinx-demo.p12" 34 | } 35 | }, 36 | "air": 37 | { 38 | "output": "bin/Syrinx-Sound-Manager-Demo.air", 39 | "signingOptions": { 40 | "storetype": "pkcs12", 41 | "keystore": "cert/syrinx-demo.p12" 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /demo/cert/syrinx-demo.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/cert/syrinx-demo.p12 -------------------------------------------------------------------------------- /demo/libs/AeonDesktopTheme.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/libs/AeonDesktopTheme.swc -------------------------------------------------------------------------------- /demo/libs/MetalWorksDesktopTheme.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/libs/MetalWorksDesktopTheme.swc -------------------------------------------------------------------------------- /demo/libs/MinimalDesktopTheme.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/libs/MinimalDesktopTheme.swc -------------------------------------------------------------------------------- /demo/libs/feathers.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/libs/feathers.swc -------------------------------------------------------------------------------- /demo/libs/starling.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/libs/starling.swc -------------------------------------------------------------------------------- /demo/media/images/Syrinx-Sound-Manager-Demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/images/Syrinx-Sound-Manager-Demo.png -------------------------------------------------------------------------------- /demo/media/sounds/piano_IEEE32_mono_44100.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_IEEE32_mono_44100.wav -------------------------------------------------------------------------------- /demo/media/sounds/piano_IEEE32_stereo_44100.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_IEEE32_stereo_44100.wav -------------------------------------------------------------------------------- /demo/media/sounds/piano_MP3_mono_44100.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_MP3_mono_44100.mp3 -------------------------------------------------------------------------------- /demo/media/sounds/piano_MP3_stereo_44100.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_MP3_stereo_44100.mp3 -------------------------------------------------------------------------------- /demo/media/sounds/piano_PCM16_mono_44100.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_PCM16_mono_44100.wav -------------------------------------------------------------------------------- /demo/media/sounds/piano_PCM16_stereo_44100.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/demo/media/sounds/piano_PCM16_stereo_44100.wav -------------------------------------------------------------------------------- /demo/src/ch/adolio/Main.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio 12 | { 13 | import flash.display.Sprite; 14 | import starling.core.Starling; 15 | 16 | public class Main extends Sprite 17 | { 18 | public function Main() 19 | { 20 | // Setup targetted frame rate 21 | stage.frameRate = 60; 22 | 23 | // Setup Starling 24 | var starling:Starling = new Starling(StarlingMain, stage); 25 | starling.start(); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/StarlingMain.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio 12 | { 13 | import feathers.themes.AeonDesktopTheme; 14 | import starling.display.Sprite; 15 | import ch.adolio.sound.SoundManagerTest; 16 | import feathers.themes.MetalWorksDesktopTheme; 17 | import feathers.themes.MinimalDesktopTheme; 18 | 19 | public class StarlingMain extends Sprite 20 | { 21 | public function StarlingMain() 22 | { 23 | // Theme selection 24 | //new AeonDesktopTheme(); 25 | //new MetalWorksDesktopTheme(); 26 | new MinimalDesktopTheme(); 27 | 28 | // Add sound test scene 29 | addChild(new SoundManagerTest()); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/sound/SoundInstanceEntry.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import ch.adolio.sound.SoundInstance; 14 | import feathers.controls.Button; 15 | import feathers.controls.Check; 16 | import feathers.controls.Label; 17 | import feathers.controls.LayoutGroup; 18 | import feathers.controls.Slider; 19 | import feathers.events.FeathersEventType; 20 | import feathers.layout.HorizontalLayout; 21 | import org.osflash.signals.Signal; 22 | import starling.animation.IAnimatable; 23 | import starling.core.Starling; 24 | import starling.display.Quad; 25 | import starling.display.Sprite; 26 | import starling.events.Event; 27 | 28 | public class SoundInstanceEntry extends Sprite implements IAnimatable 29 | { 30 | public var _soundInstance:SoundInstance; 31 | 32 | private var _soundNameLabel:Label; 33 | private var _pauseResumeButton:Button; 34 | private var _stopButton:Button; 35 | private var _destroyButton:Button; 36 | private var _positionSlider:Slider; 37 | private var _loopsLabel:Label; 38 | private var _volumeSlider:Slider; 39 | private var _muteCheck:Check; 40 | private var _panSlider:Slider; 41 | private var _pitchSlider:Slider; 42 | 43 | public var _isPositionSliderGrabbed:Object; 44 | public var _wasPlayingWhenGrabbed:Object; 45 | 46 | public var playStatusUpdated:Signal = new Signal(SoundInstanceEntry); 47 | 48 | public function SoundInstanceEntry(soundInstance:SoundInstance) 49 | { 50 | _soundInstance = soundInstance; 51 | _soundInstance.started.add(onSoundStartedToBePlayed); 52 | _soundInstance.paused.add(onSoundPaused); 53 | _soundInstance.resumed.add(onSoundResumed); 54 | _soundInstance.stopped.add(onSoundStopped); 55 | _soundInstance.completed.add(onSoundCompleted); 56 | _soundInstance.destroyed.add(onSoundDestroyed); 57 | 58 | // Background 59 | var quad:Quad = new Quad(Starling.current.stage.stageWidth, 30, 0xffffff); 60 | quad.alpha = 0.6; 61 | addChild(quad); 62 | 63 | // Horizontal layout 64 | var layout:HorizontalLayout = new HorizontalLayout(); 65 | layout.padding = 5; 66 | layout.gap = 5; 67 | var container:LayoutGroup = new LayoutGroup(); 68 | container.layout = layout; 69 | container.width = quad.width; 70 | container.height = quad.height; 71 | this.addChild(container); 72 | 73 | // Sound label 74 | _soundNameLabel = new Label(); 75 | _soundNameLabel.width = 160; 76 | _soundNameLabel.text = "#" + _soundInstance.id + " (" + _soundInstance.type + ")"; 77 | container.addChild(_soundNameLabel); 78 | 79 | // Position slider 80 | _positionSlider = new Slider(); 81 | _positionSlider.minimum = 0; 82 | _positionSlider.maximum = 1.0; 83 | _positionSlider.value = 0; 84 | _positionSlider.width = 200; 85 | _positionSlider.liveDragging = false; // Only dispatch change event when releasing thumb 86 | container.addChild(_positionSlider); 87 | _positionSlider.addEventListener(Event.CHANGE, onPositionValueChanged); 88 | _positionSlider.addEventListener(FeathersEventType.BEGIN_INTERACTION, onPositionSliderSelected); 89 | _positionSlider.addEventListener(FeathersEventType.END_INTERACTION, onPositionSliderReleased); 90 | 91 | // Loops label 92 | _loopsLabel = new Label(); 93 | _loopsLabel.width = 40; 94 | container.addChild(_loopsLabel); 95 | 96 | // Volume slider 97 | _volumeSlider = new Slider(); 98 | _volumeSlider.minimum = 0; 99 | _volumeSlider.maximum = 1.0; 100 | _volumeSlider.value = _soundInstance.volume; 101 | _volumeSlider.width = 100; 102 | container.addChild(_volumeSlider); 103 | _volumeSlider.addEventListener(Event.CHANGE, onVolumeValueChanged); 104 | 105 | // Mute 106 | _muteCheck = new Check(); 107 | _muteCheck.width = 30; 108 | container.addChild(_muteCheck); 109 | _muteCheck.addEventListener(Event.CHANGE, onMuteValueChanged); 110 | 111 | // Pan slider 112 | _panSlider = new Slider(); 113 | _panSlider.minimum = -1.0; 114 | _panSlider.maximum = 1.0; 115 | _panSlider.value = _soundInstance.pan; 116 | _panSlider.width = 100; 117 | container.addChild(_panSlider); 118 | _panSlider.addEventListener(Event.CHANGE, onPanValueChanged); 119 | 120 | // Pitch 121 | _pitchSlider = new Slider(); 122 | _pitchSlider.minimum = 0.5; 123 | _pitchSlider.maximum = 2.0; 124 | _pitchSlider.value = _soundInstance.pitch; 125 | _pitchSlider.width = 100; 126 | container.addChild(_pitchSlider); 127 | _pitchSlider.addEventListener(Event.CHANGE, onPitchValueChanged); 128 | 129 | // Pause / resume button 130 | _pauseResumeButton = new Button(); 131 | updatePauseResumeButtonText(); 132 | _pauseResumeButton.width = 60; 133 | _pauseResumeButton.addEventListener(Event.TRIGGERED, onPauseResumeButtonTriggered); 134 | container.addChild(_pauseResumeButton); 135 | 136 | // Stop button 137 | _stopButton = new Button(); 138 | _stopButton.label = "Stop"; 139 | _stopButton.width = 60; 140 | _stopButton.addEventListener(Event.TRIGGERED, onStopButtonTriggered); 141 | container.addChild(_stopButton); 142 | 143 | // Destroy button 144 | _destroyButton = new Button(); 145 | _destroyButton.label = "X"; 146 | _destroyButton.width = 60; 147 | _destroyButton.addEventListener(Event.TRIGGERED, onDestroyButtonTriggered); 148 | container.addChild(_destroyButton); 149 | 150 | Starling.current.juggler.add(this); 151 | 152 | // Initial update 153 | updatePosition(); 154 | updateLoopingStatus(); 155 | } 156 | 157 | public function get soundInstance():SoundInstance 158 | { 159 | return _soundInstance; 160 | } 161 | 162 | public function advanceTime(time:Number):void 163 | { 164 | // Do not update when the sound instance is paused 165 | if (_soundInstance.isPaused) 166 | return; 167 | 168 | // Do not update when slider is grabbed by user 169 | if (_isPositionSliderGrabbed) 170 | return; 171 | 172 | updatePosition(); 173 | updateLoopingStatus(); 174 | } 175 | 176 | private function onSoundStartedToBePlayed(si:SoundInstance):void 177 | { 178 | updatePauseResumeButtonText(); 179 | 180 | // Enable / disable position slider according looping options 181 | _positionSlider.isEnabled = _soundInstance.loopsRemaining != -1; 182 | 183 | // Update UI volume 184 | _volumeSlider.value = _soundInstance.volume; // TODO Do it silently to avoid re-updating the value 185 | 186 | playStatusUpdated.dispatch(this); 187 | } 188 | 189 | private function onSoundPaused(si:SoundInstance):void 190 | { 191 | updatePauseResumeButtonText(); 192 | 193 | playStatusUpdated.dispatch(this); 194 | } 195 | 196 | private function onSoundResumed(si:SoundInstance):void 197 | { 198 | updatePauseResumeButtonText(); 199 | 200 | playStatusUpdated.dispatch(this); 201 | } 202 | 203 | private function onSoundStopped(si:SoundInstance):void 204 | { 205 | updatePauseResumeButtonText(); 206 | updateLoopingStatus(); 207 | 208 | playStatusUpdated.dispatch(this); 209 | } 210 | 211 | private function onSoundCompleted(si:SoundInstance):void 212 | { 213 | playStatusUpdated.dispatch(this); 214 | } 215 | 216 | private function onSoundDestroyed(si:SoundInstance):void 217 | { 218 | clearSound(); 219 | } 220 | 221 | public function clearSound():void 222 | { 223 | // Reset sound instance reference (no need to unsubscribe to events since the object is destroyed) 224 | _soundInstance = null; 225 | 226 | // Stop refreshing 227 | Starling.current.juggler.remove(this); 228 | } 229 | 230 | private function onPositionValueChanged(event:Event):void 231 | { 232 | _soundInstance.positionRatio = _positionSlider.value; 233 | } 234 | 235 | private function onPositionSliderSelected(event:Event):void 236 | { 237 | _isPositionSliderGrabbed = true; 238 | 239 | // Pause 240 | _wasPlayingWhenGrabbed = !_soundInstance.isPaused; 241 | if (!_soundInstance.isPaused) 242 | _soundInstance.pause(); 243 | } 244 | 245 | private function onPositionSliderReleased(event:Event):void 246 | { 247 | _isPositionSliderGrabbed = false; 248 | 249 | // Reset back playing status 250 | if (_wasPlayingWhenGrabbed && !_soundInstance.isDestroyed) 251 | _soundInstance.resume(); 252 | } 253 | 254 | private function onPauseResumeButtonTriggered():void 255 | { 256 | if (_soundInstance) 257 | { 258 | if (_soundInstance.isStarted) 259 | { 260 | // Resume 261 | if (_soundInstance.isPaused) 262 | { 263 | _soundInstance.resume(); 264 | } 265 | // Pause 266 | else 267 | { 268 | _soundInstance.pause(); 269 | } 270 | } 271 | else 272 | { 273 | // Play 274 | _soundInstance.play(_soundInstance.volume, _positionSlider.value * _soundInstance.totalLength, _soundInstance.loops); 275 | } 276 | } 277 | } 278 | 279 | private function updatePauseResumeButtonText():void 280 | { 281 | if (!_soundInstance) 282 | return; 283 | 284 | if (_soundInstance.isStarted) 285 | { 286 | if (_soundInstance.isPaused) 287 | { 288 | _pauseResumeButton.label = "Resume"; 289 | } 290 | else 291 | { 292 | _pauseResumeButton.label = "Pause"; 293 | } 294 | } 295 | else 296 | { 297 | _pauseResumeButton.label = "Play"; 298 | } 299 | } 300 | 301 | private function onStopButtonTriggered():void 302 | { 303 | _soundInstance.stop(); 304 | } 305 | 306 | private function onDestroyButtonTriggered():void 307 | { 308 | // Destroy or release the sound instance 309 | if (_soundInstance.manager.isPoolingEnabled && _soundInstance.isFromPool) 310 | _soundInstance.manager.releaseSoundInstanceToPool(_soundInstance); 311 | else 312 | _soundInstance.destroy(); 313 | } 314 | 315 | private function onVolumeValueChanged(event:Event):void 316 | { 317 | _soundInstance.volume = _volumeSlider.value; 318 | } 319 | 320 | private function onMuteValueChanged(event:Event):void 321 | { 322 | _soundInstance.isMuted = _muteCheck.isSelected; 323 | } 324 | 325 | private function onPanValueChanged(event:Event):void 326 | { 327 | _soundInstance.pan = _panSlider.value; 328 | } 329 | 330 | private function onPitchValueChanged(event:Event):void 331 | { 332 | _soundInstance.pitch = _pitchSlider.value; 333 | } 334 | 335 | private function updatePosition():void 336 | { 337 | _positionSlider.removeEventListener(Event.CHANGE, onPositionValueChanged); 338 | _positionSlider.value = _soundInstance.positionRatio; 339 | _positionSlider.addEventListener(Event.CHANGE, onPositionValueChanged); 340 | } 341 | 342 | private function updateLoopingStatus():void 343 | { 344 | // Do not update when the sound instance is paused 345 | if (_soundInstance.isPaused) 346 | return; 347 | 348 | // Update loops 349 | if (_soundInstance.loops == -1) 350 | _loopsLabel.text = "∞"; 351 | else if (_soundInstance.loops == 0) 352 | _loopsLabel.text = "-"; 353 | else 354 | _loopsLabel.text = _soundInstance.currentLoop + "/" + _soundInstance.loops; 355 | } 356 | } 357 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/sound/SoundInstanceEntryHeader.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import feathers.controls.Label; 14 | import feathers.controls.LayoutGroup; 15 | import feathers.layout.HorizontalLayout; 16 | import starling.display.Quad; 17 | import starling.display.Sprite; 18 | import starling.core.Starling; 19 | 20 | public class SoundInstanceEntryHeader extends Sprite 21 | { 22 | private var _soundNameLabel:Label; 23 | private var _pauseResumeLabel:Label; 24 | private var _stopLabel:Label; 25 | private var _positionLabel:Label; 26 | private var _muteLabel:Label; 27 | 28 | public function SoundInstanceEntryHeader() 29 | { 30 | // background 31 | var quad:Quad = new Quad(Starling.current.stage.stageWidth, 30, 0xffffff); 32 | quad.alpha = 0.8; 33 | addChild(quad); 34 | 35 | // horizontal layout 36 | var layout:HorizontalLayout = new HorizontalLayout(); 37 | layout.padding = 5; 38 | layout.gap = 5; 39 | var container:LayoutGroup = new LayoutGroup(); 40 | container.layout = layout; 41 | container.width = quad.width; 42 | container.height = quad.height; 43 | this.addChild(container); 44 | 45 | // sound label 46 | _soundNameLabel = new Label(); 47 | _soundNameLabel.width = 160; 48 | _soundNameLabel.text = "Id (Type)"; 49 | container.addChild(_soundNameLabel); 50 | 51 | // position slider 52 | _positionLabel = new Label(); 53 | _positionLabel.text = "Position"; 54 | _positionLabel.width = 200; 55 | container.addChild(_positionLabel); 56 | 57 | // loops label 58 | var loopsLabel:Label = new Label(); 59 | loopsLabel.text = "Loops"; 60 | loopsLabel.width = 40; 61 | container.addChild(loopsLabel); 62 | 63 | // volume label 64 | var volumeLabel:Label = new Label(); 65 | volumeLabel.text = "Volume"; 66 | volumeLabel.width = 100; 67 | container.addChild(volumeLabel); 68 | 69 | // Mute 70 | _muteLabel = new Label(); 71 | _muteLabel.text = "Mute"; 72 | _muteLabel.width = 30; 73 | container.addChild(_muteLabel); 74 | 75 | // Pan label 76 | var panLabel:Label = new Label(); 77 | panLabel.text = "Pan"; 78 | panLabel.width = 100; 79 | container.addChild(panLabel); 80 | 81 | // Pitch label 82 | var pitchLabel:Label = new Label(); 83 | pitchLabel.text = "Pitch"; 84 | pitchLabel.width = 100; 85 | container.addChild(pitchLabel); 86 | 87 | // Pause / resume label 88 | _pauseResumeLabel = new Label(); 89 | _pauseResumeLabel.width = 60; 90 | container.addChild(_pauseResumeLabel); 91 | 92 | // Stop button 93 | _stopLabel = new Label(); 94 | _stopLabel.width = 60; 95 | container.addChild(_stopLabel); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/sound/SoundManagerTest.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import ch.adolio.sound.Mp3Track; 14 | import ch.adolio.sound.SoundInstance; 15 | import ch.adolio.sound.SoundManager; 16 | import ch.adolio.sound.TrackConfiguration; 17 | import ch.adolio.sound.WavTrack; 18 | import feathers.controls.Button; 19 | import feathers.controls.Label; 20 | import feathers.controls.ScrollBarDisplayMode; 21 | import feathers.controls.ScrollContainer; 22 | import feathers.controls.Slider; 23 | import feathers.layout.VerticalLayout; 24 | import flash.events.Event; 25 | import flash.filesystem.File; 26 | import flash.filesystem.FileMode; 27 | import flash.filesystem.FileStream; 28 | import flash.media.Sound; 29 | import flash.net.FileFilter; 30 | import flash.net.URLRequest; 31 | import flash.utils.ByteArray; 32 | import starling.core.Starling; 33 | import starling.display.Sprite; 34 | import starling.events.Event; 35 | 36 | public class SoundManagerTest extends Sprite 37 | { 38 | // Sound Manager 39 | private var _sndMgr:SoundManager; 40 | private var _masterVolumeSlider:Slider; 41 | 42 | // Loading 43 | private var _loadMp3SoundButton:Button; 44 | private var _loadWavSoundButton:Button; 45 | private var _currentFileRef:File; 46 | 47 | // Registered sounds 48 | private var _trackConfigurationEntries:Vector. = new Vector.(); 49 | private var _registeredTrackLabel:Label; 50 | private var _trackConfigurationEntriesContainer:ScrollContainer; 51 | private var _soundConfigurationEntryHeader:TrackConfigurationEntryHeader; 52 | 53 | // Running sounds 54 | private var _soundInstanceEntries:Vector. = new Vector.(); 55 | private var _runningSoundsLabel:Label; 56 | private var _soundInstanceEntriesContainer:ScrollContainer; 57 | private var _soundInstanceEntryHeader:SoundInstanceEntryHeader; 58 | 59 | // Pool 60 | private var _poolInfoLabel:Label; 61 | 62 | // Embedded sounds source 63 | [Embed(source = "../../../../media/sounds/piano_IEEE32_mono_44100.wav", mimeType="application/octet-stream")] public static const piano_IEEE32_mono_44100:Class; 64 | [Embed(source = "../../../../media/sounds/piano_IEEE32_stereo_44100.wav", mimeType="application/octet-stream")] public static const piano_IEEE32_stereo_44100:Class; 65 | [Embed(source = "../../../../media/sounds/piano_PCM16_mono_44100.wav", mimeType="application/octet-stream")] public static const piano_PCM16_mono_44100:Class; 66 | [Embed(source = "../../../../media/sounds/piano_PCM16_stereo_44100.wav", mimeType="application/octet-stream")] public static const piano_PCM16_stereo_44100:Class; 67 | [Embed(source = "../../../../media/sounds/piano_MP3_mono_44100.mp3")] public static const piano_MP3_mono_44100:Class; 68 | [Embed(source = "../../../../media/sounds/piano_MP3_stereo_44100.mp3")] public static const piano_MP3_stereo_44100:Class; 69 | 70 | public function SoundManagerTest() 71 | { 72 | super(); 73 | 74 | // UI optimization when doing nothing 75 | Starling.current.skipUnchangedFrames = true; 76 | 77 | // Create sound manager 78 | _sndMgr = new SoundManager(); 79 | //_sndMgr.maxChannelCapacity = 3; // Limit number of simultaneous playing sounds 80 | _sndMgr.isPoolingEnabled = true; // Enable pooling 81 | 82 | // Register to events 83 | _sndMgr.trackRegistered.add(onTrackRegisteredToManager); 84 | _sndMgr.trackUnregistered.add(onTrackUnregisteredFromManager); 85 | _sndMgr.soundInstanceAdded.add(onSoundInstanceAddedToManager); 86 | _sndMgr.soundInstanceRemoved.add(onSoundInstanceRemoveFromManager); 87 | _sndMgr.soundInstanceReleasedToPool.add(onSoundInstanceReleasedToPool); 88 | 89 | // Setting up the UI 90 | setupUI(); 91 | 92 | // Register sounds 93 | var predecodeWav:Boolean = false; 94 | _sndMgr.registerTrack("piano_IEEE32_mono_44100", new WavTrack(new piano_IEEE32_mono_44100(), predecodeWav)); 95 | _sndMgr.registerTrack("piano_IEEE32_stereo_44100", new WavTrack(new piano_IEEE32_stereo_44100(), predecodeWav)); 96 | _sndMgr.registerTrack("piano_PCM16_mono_44100", new WavTrack(new piano_PCM16_mono_44100(), predecodeWav)); 97 | _sndMgr.registerTrack("piano_PCM16_stereo_44100", new WavTrack(new piano_PCM16_stereo_44100(), predecodeWav)); 98 | _sndMgr.registerTrack("piano_MP3_mono_44100", new Mp3Track(new piano_MP3_mono_44100())); 99 | _sndMgr.registerTrack("piano_MP3_stereo_44100", new Mp3Track(new piano_MP3_stereo_44100())); 100 | } 101 | 102 | private function setupUI():void 103 | { 104 | //----------------------------------------------------------------- 105 | //-- Title 106 | //----------------------------------------------------------------- 107 | 108 | // Volume label 109 | var titleLable:Label = new Label(); 110 | titleLable.text = "Syrinx - Sound Manager Demo"; 111 | titleLable.x = 8; 112 | titleLable.y = 16; 113 | titleLable.validate(); 114 | addChild(titleLable); 115 | 116 | //----------------------------------------------------------------- 117 | //-- Sound Manager 118 | //----------------------------------------------------------------- 119 | 120 | // Volume label 121 | var masterVolumeLabel:Label = new Label(); 122 | masterVolumeLabel.text = "Master volume"; 123 | masterVolumeLabel.x = 8; 124 | masterVolumeLabel.y = titleLable.y + titleLable.height + 16; 125 | masterVolumeLabel.validate(); 126 | addChild(masterVolumeLabel); 127 | 128 | // Volume slider 129 | _masterVolumeSlider = new Slider(); 130 | _masterVolumeSlider.minimum = 0; 131 | _masterVolumeSlider.maximum = 1.0; 132 | _masterVolumeSlider.value = 0.5; 133 | _masterVolumeSlider.x = masterVolumeLabel.x + masterVolumeLabel.width + 8; 134 | _masterVolumeSlider.y = masterVolumeLabel.y; 135 | _masterVolumeSlider.addEventListener(starling.events.Event.CHANGE, onMasterVolumeValueChanged); 136 | addChild(_masterVolumeSlider); 137 | 138 | //----------------------------------------------------------------- 139 | //-- Registered tracks 140 | //----------------------------------------------------------------- 141 | 142 | // Registered sounds label 143 | _registeredTrackLabel = new Label(); 144 | _registeredTrackLabel.text = "Registered tracks"; 145 | _registeredTrackLabel.x = 8; 146 | _registeredTrackLabel.y = masterVolumeLabel.y + masterVolumeLabel.height + 16; 147 | _registeredTrackLabel.validate(); 148 | addChild(_registeredTrackLabel); 149 | 150 | // Load MP3 151 | _loadMp3SoundButton = new Button(); 152 | _loadMp3SoundButton.label = "Load MP3"; 153 | _loadMp3SoundButton.validate(); 154 | _loadMp3SoundButton.x = 16; 155 | _loadMp3SoundButton.y = _registeredTrackLabel.y + _registeredTrackLabel.height + 8; 156 | _loadMp3SoundButton.addEventListener(starling.events.Event.TRIGGERED, onLoadMp3SoundTriggered); 157 | addChild(_loadMp3SoundButton); 158 | 159 | // Load WAV 160 | _loadWavSoundButton = new Button(); 161 | _loadWavSoundButton.label = "Load WAV"; 162 | _loadWavSoundButton.validate(); 163 | _loadWavSoundButton.x = _loadMp3SoundButton.x + _loadMp3SoundButton.width + 8; 164 | _loadWavSoundButton.y = _registeredTrackLabel.y + _registeredTrackLabel.height + 8; 165 | _loadWavSoundButton.addEventListener(starling.events.Event.TRIGGERED, onLoadWavSoundTriggered); 166 | addChild(_loadWavSoundButton); 167 | 168 | // Running sounds 169 | var soundConfigurationEntriesLayout:VerticalLayout = new VerticalLayout(); 170 | soundConfigurationEntriesLayout.gap = 1; 171 | _trackConfigurationEntriesContainer = new ScrollContainer(); 172 | _trackConfigurationEntriesContainer.layout = soundConfigurationEntriesLayout; 173 | _trackConfigurationEntriesContainer.scrollBarDisplayMode = ScrollBarDisplayMode.FLOAT; 174 | _trackConfigurationEntriesContainer.x = 16; 175 | _trackConfigurationEntriesContainer.y = _loadMp3SoundButton.y + _loadMp3SoundButton.height + 8; 176 | _trackConfigurationEntriesContainer.width = Starling.current.stage.stageWidth; 177 | _trackConfigurationEntriesContainer.height = 240; 178 | addChild(_trackConfigurationEntriesContainer); 179 | 180 | // Setup header 181 | _soundConfigurationEntryHeader = new TrackConfigurationEntryHeader(); 182 | _trackConfigurationEntriesContainer.addChild(_soundConfigurationEntryHeader); 183 | 184 | //----------------------------------------------------------------- 185 | //-- Running sounds 186 | //----------------------------------------------------------------- 187 | 188 | // Registered sounds label 189 | _runningSoundsLabel = new Label(); 190 | _runningSoundsLabel.text = "Running sounds"; 191 | _runningSoundsLabel.x = 8; 192 | _runningSoundsLabel.y = _trackConfigurationEntriesContainer.y + _trackConfigurationEntriesContainer.height + 8; 193 | _runningSoundsLabel.validate(); 194 | addChild(_runningSoundsLabel); 195 | 196 | // Running sounds 197 | var soundInstanceEntriesLayout:VerticalLayout = new VerticalLayout(); 198 | soundConfigurationEntriesLayout.gap = 1; 199 | _soundInstanceEntriesContainer = new ScrollContainer(); 200 | _soundInstanceEntriesContainer.layout = soundConfigurationEntriesLayout; 201 | _soundInstanceEntriesContainer.scrollBarDisplayMode = ScrollBarDisplayMode.FLOAT; 202 | _soundInstanceEntriesContainer.x = 16; 203 | _soundInstanceEntriesContainer.y = _runningSoundsLabel.y + _runningSoundsLabel.height + 8; 204 | _soundInstanceEntriesContainer.width = Starling.current.stage.stageWidth; 205 | _soundInstanceEntriesContainer.height = 400; 206 | addChild(_soundInstanceEntriesContainer); 207 | 208 | // Setup header 209 | _soundInstanceEntryHeader = new SoundInstanceEntryHeader(); 210 | _soundInstanceEntriesContainer.addChild(_soundInstanceEntryHeader); 211 | 212 | //----------------------------------------------------------------- 213 | //-- Pool info 214 | //----------------------------------------------------------------- 215 | 216 | _poolInfoLabel = new Label(); 217 | _poolInfoLabel.text = "Placeholder text"; 218 | _poolInfoLabel.validate(); 219 | _poolInfoLabel.x = 8; 220 | _poolInfoLabel.y = Starling.current.stage.stageHeight - _poolInfoLabel.height - 8; 221 | addChild(_poolInfoLabel); 222 | 223 | updatePoolInfoLabel(); 224 | } 225 | 226 | public function playSound(type:String, volume:Number = 1.0, startTime:Number = 0, loops:int = 0):SoundInstance 227 | { 228 | return _sndMgr.play(type, volume, startTime, loops); 229 | } 230 | 231 | public function unregisterSound(soundConfiguration:TrackConfiguration):void 232 | { 233 | _sndMgr.unregisterTrack(soundConfiguration.type); 234 | } 235 | 236 | public function updateRunningSoundsLabel():void 237 | { 238 | _runningSoundsLabel.text = "Running sounds (" + _sndMgr.getPlayingSoundInstancesCount() + "/" + _sndMgr.getSoundInstancesCount() + ")"; 239 | } 240 | 241 | public function updatePoolInfoLabel():void 242 | { 243 | _poolInfoLabel.text = "Pool info (acquired: " + SoundInstancesPool.instance.acquiredCount + ", released: " + SoundInstancesPool.instance.releaseCount + ", capacity: " + SoundInstancesPool.instance.capacity + ")"; 244 | } 245 | 246 | private function onLoadMp3SoundTriggered(event:starling.events.Event):void 247 | { 248 | loadMp3SoundFromDisk(); 249 | } 250 | 251 | private function onLoadWavSoundTriggered(event:starling.events.Event):void 252 | { 253 | loadWavSoundFromDisk(); 254 | } 255 | 256 | private function onMasterVolumeValueChanged(event:starling.events.Event):void 257 | { 258 | _sndMgr.volume = _masterVolumeSlider.value; 259 | } 260 | 261 | private function onTrackRegisteredToManager(tc:TrackConfiguration):void 262 | { 263 | // Create entry & register sound 264 | var entry:TrackConfigurationEntry = new TrackConfigurationEntry(tc, this); 265 | _trackConfigurationEntriesContainer.addChild(entry); 266 | _trackConfigurationEntries.push(entry); 267 | 268 | // Update running tracks label 269 | _registeredTrackLabel.text = "Registered tracks (" + _sndMgr.getRegisteredTracks().length + ")"; 270 | } 271 | 272 | private function onTrackUnregisteredFromManager(tc:TrackConfiguration):void 273 | { 274 | // Remove completed sound instance entry 275 | for (var i:int = 0; i < _trackConfigurationEntries.length; ++i) 276 | { 277 | if (_trackConfigurationEntries[i].trackConfiguration == tc) 278 | { 279 | _trackConfigurationEntriesContainer.removeChild(_trackConfigurationEntries[i]); 280 | _trackConfigurationEntries.removeAt(i); 281 | break; 282 | } 283 | } 284 | 285 | _registeredTrackLabel.text = "Registered tracks (" + _sndMgr.getRegisteredTracks().length + ")"; 286 | } 287 | 288 | private function onSoundInstanceAddedToManager(si:SoundInstance):void 289 | { 290 | // Create entry & register sound 291 | var entry:SoundInstanceEntry = new SoundInstanceEntry(si); 292 | entry.playStatusUpdated.add(onSoundInstanceEntryPlayStatusUpdated); 293 | _soundInstanceEntriesContainer.addChild(entry); 294 | _soundInstanceEntries.push(entry); 295 | 296 | // Update running sounds label 297 | updateRunningSoundsLabel(); 298 | updatePoolInfoLabel(); 299 | } 300 | 301 | private function onSoundInstanceEntryPlayStatusUpdated(entry:SoundInstanceEntry):void 302 | { 303 | // Update running sounds label 304 | updateRunningSoundsLabel(); 305 | } 306 | 307 | private function onSoundInstanceRemoveFromManager(si:SoundInstance):void 308 | { 309 | removeEntryForSoundInstance(si); 310 | } 311 | 312 | private function onSoundInstanceReleasedToPool(si:SoundInstance, pool:SoundInstancesPool):void 313 | { 314 | removeEntryForSoundInstance(si); 315 | } 316 | 317 | private function removeEntryForSoundInstance(si:SoundInstance):void 318 | { 319 | // Find & remove sound instance entry 320 | for (var i:int = 0; i < _soundInstanceEntries.length; ++i) 321 | { 322 | var entry:SoundInstanceEntry = _soundInstanceEntries[i]; 323 | if (entry.soundInstance == si) 324 | { 325 | // Remove from lists 326 | _soundInstanceEntriesContainer.removeChild(entry); 327 | _soundInstanceEntries.removeAt(i); 328 | 329 | // Unregister from events 330 | entry.playStatusUpdated.remove(onSoundInstanceEntryPlayStatusUpdated); 331 | 332 | // No need to look for more entries 333 | break; 334 | } 335 | } 336 | 337 | // Update labels 338 | updateRunningSoundsLabel(); 339 | updatePoolInfoLabel(); 340 | } 341 | 342 | //----------------------------------------------------------------- 343 | //-- MP3 File loading 344 | //----------------------------------------------------------------- 345 | 346 | private function loadMp3SoundFromDisk():void 347 | { 348 | _currentFileRef = new File(); 349 | 350 | _currentFileRef.addEventListener(flash.events.Event.SELECT, onMp3FileSelected); 351 | _currentFileRef.addEventListener(flash.events.Event.CANCEL, onMp3FileSelectionCanceled); 352 | 353 | var imageFileTypes:FileFilter = new FileFilter("MP3 (*.mp3)", "*.mp3"); 354 | _currentFileRef.browse([imageFileTypes]); 355 | } 356 | 357 | private function onMp3FileSelectionCanceled(event:flash.events.Event):void 358 | { 359 | _currentFileRef.removeEventListener(flash.events.Event.SELECT, onMp3FileSelected); 360 | _currentFileRef.removeEventListener(flash.events.Event.CANCEL, onMp3FileSelectionCanceled); 361 | 362 | _currentFileRef = null; 363 | } 364 | 365 | private function onMp3FileSelected(e:Object):void 366 | { 367 | _currentFileRef.removeEventListener(flash.events.Event.SELECT, onMp3FileSelected); 368 | _currentFileRef.removeEventListener(flash.events.Event.CANCEL, onMp3FileSelectionCanceled); 369 | 370 | // Load sound for path 371 | var sound:Sound = new Sound(); 372 | sound.load(new URLRequest(_currentFileRef.url)); 373 | sound.addEventListener(flash.events.Event.COMPLETE, onMp3SoundLoaded); 374 | } 375 | 376 | private function onMp3SoundLoaded(event:flash.events.Event):void 377 | { 378 | try 379 | { 380 | // Register loaded sound 381 | _sndMgr.registerTrack(_currentFileRef.name, new Mp3Track(event.target as Sound)); 382 | } 383 | catch (e:ArgumentError) 384 | { 385 | trace("[Sound Test] Couldn't register MP3 track. Error: " + e.message); 386 | } 387 | } 388 | 389 | //----------------------------------------------------------------- 390 | //-- WAV File loading 391 | //----------------------------------------------------------------- 392 | 393 | private function loadWavSoundFromDisk():void 394 | { 395 | _currentFileRef = new File(); 396 | 397 | _currentFileRef.addEventListener(flash.events.Event.SELECT, onWavFileSelected); 398 | _currentFileRef.addEventListener(flash.events.Event.CANCEL, onWavFileSelectionCanceled); 399 | 400 | var imageFileTypes:FileFilter = new FileFilter("WAV (*.wav)", "*.wav"); 401 | _currentFileRef.browse([imageFileTypes]); 402 | } 403 | 404 | private function onWavFileSelectionCanceled(event:flash.events.Event):void 405 | { 406 | _currentFileRef.removeEventListener(flash.events.Event.SELECT, onWavFileSelected); 407 | _currentFileRef.removeEventListener(flash.events.Event.CANCEL, onWavFileSelectionCanceled); 408 | 409 | _currentFileRef = null; 410 | } 411 | 412 | private function onWavFileSelected(e:Object):void 413 | { 414 | _currentFileRef.removeEventListener(flash.events.Event.SELECT, onWavFileSelected); 415 | _currentFileRef.removeEventListener(flash.events.Event.CANCEL, onWavFileSelectionCanceled); 416 | 417 | // Load sound for path 418 | var fileStream:FileStream = new FileStream(); 419 | fileStream.open(_currentFileRef, FileMode.READ); 420 | var bytes:ByteArray = new ByteArray(); 421 | fileStream.readBytes(bytes); 422 | fileStream.close(); 423 | 424 | try 425 | { 426 | // Register loaded sound 427 | _sndMgr.registerTrack(_currentFileRef.name, new WavTrack(bytes)); 428 | } 429 | catch (e:ArgumentError) 430 | { 431 | trace("[Sound Test] Couldn't register Wav track. Error: " + e.message); 432 | } 433 | } 434 | } 435 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/sound/TrackConfigurationEntry.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import ch.adolio.sound.TrackConfiguration; 14 | import feathers.controls.Button; 15 | import feathers.controls.Label; 16 | import feathers.controls.LayoutGroup; 17 | import feathers.controls.NumericStepper; 18 | import feathers.controls.Slider; 19 | import feathers.controls.TextInput; 20 | import feathers.layout.HorizontalLayout; 21 | import starling.core.Starling; 22 | import starling.display.Quad; 23 | import starling.display.Sprite; 24 | import starling.events.Event; 25 | 26 | public class TrackConfigurationEntry extends Sprite 27 | { 28 | // Core references 29 | private var _trackConfiguration:TrackConfiguration; 30 | private var _soundTest:SoundManagerTest; 31 | 32 | // configuration 33 | private var _soundTypeLabel:Label; 34 | private var _baseVolumeSlider:Slider; 35 | private var _soundLength:Label; 36 | private var _trimStartTextInput:TextInput; 37 | private var _trimEndTextInput:TextInput; 38 | private var _trimButton:Button; 39 | 40 | // control 41 | private var _loopStepper:NumericStepper; 42 | private var _startPositionTextInput:TextInput; 43 | //private var _startVolumeSlider:Slider; // TODO 44 | private var _playButton:Button; 45 | private var _unregisterButton:Button; 46 | 47 | public function TrackConfigurationEntry(trackConfiguration:TrackConfiguration, soundTest:SoundManagerTest) 48 | { 49 | _trackConfiguration = trackConfiguration; 50 | _soundTest = soundTest; 51 | 52 | // background 53 | var quad:Quad = new Quad(Starling.current.stage.stageWidth, 30, 0xffffff); 54 | quad.alpha = 0.6; 55 | addChild(quad); 56 | 57 | // horizontal layout 58 | var layout:HorizontalLayout = new HorizontalLayout(); 59 | layout.padding = 2; 60 | layout.gap = 4; 61 | var container:LayoutGroup = new LayoutGroup(); 62 | container.layout = layout; 63 | container.width = quad.width; 64 | container.height = quad.height; 65 | this.addChild(container); 66 | 67 | // sound label 68 | _soundTypeLabel = new Label(); 69 | _soundTypeLabel.width = 160; 70 | _soundTypeLabel.height = quad.height - (layout.paddingTop + layout.paddingBottom); 71 | _soundTypeLabel.text = _trackConfiguration.type; 72 | container.addChild(_soundTypeLabel); 73 | 74 | // base volume 75 | _baseVolumeSlider = new Slider(); 76 | _baseVolumeSlider.minimum = 0; 77 | _baseVolumeSlider.maximum = 1.0; 78 | _baseVolumeSlider.step = 0.05; 79 | _baseVolumeSlider.width = 80; 80 | _baseVolumeSlider.value = _trackConfiguration.baseVolume; 81 | container.addChild(_baseVolumeSlider); 82 | _baseVolumeSlider.addEventListener(Event.CHANGE, onBaseVolumeChanged); 83 | 84 | // sound length in ms 85 | _soundLength = new Label(); 86 | _soundLength.width = 80; 87 | _soundLength.text = (Math.round(_trackConfiguration.track.length * 100) / 100) + ""; 88 | container.addChild(_soundLength); 89 | 90 | // Trim start text input 91 | _trimStartTextInput = new TextInput(); 92 | _trimStartTextInput.width = 80; 93 | _trimStartTextInput.text = _trackConfiguration.trimStartDuration.toString(); 94 | container.addChild(_trimStartTextInput); 95 | _trimStartTextInput.addEventListener(Event.CHANGE, onTrimStartDurationChanged); 96 | 97 | // Trim end text input 98 | _trimEndTextInput = new TextInput(); 99 | _trimEndTextInput.width = 80; 100 | _trimEndTextInput.text = _trackConfiguration.trimEndDuration.toString(); 101 | container.addChild(_trimEndTextInput); 102 | _trimEndTextInput.addEventListener(Event.CHANGE, onTrimEndDurationChanged); 103 | 104 | // Auto detect trimming values 105 | _trimButton = new Button(); 106 | _trimButton.label = "Auto Trim"; 107 | _trimButton.width = 60; 108 | _trimButton.addEventListener(Event.TRIGGERED, onTrimButtonTriggered); 109 | container.addChild(_trimButton); 110 | 111 | //----------------------------------------------------------------- 112 | //-- Controls 113 | //----------------------------------------------------------------- 114 | 115 | // Separator 116 | var separator:Quad = new Quad(20, 1); 117 | separator.alpha = 0; 118 | container.addChild(separator); 119 | 120 | // Loop input 121 | _loopStepper = new NumericStepper(); 122 | _loopStepper.minimum = -1; 123 | _loopStepper.maximum = 100; 124 | _loopStepper.step = 1; 125 | _loopStepper.width = 80; 126 | container.addChild(_loopStepper); 127 | 128 | // Start position input 129 | _startPositionTextInput = new TextInput(); 130 | _startPositionTextInput.width = 80; 131 | _startPositionTextInput.text = "0"; 132 | container.addChild(_startPositionTextInput); 133 | 134 | // play button 135 | _playButton = new Button(); 136 | _playButton.label = "Play"; 137 | _playButton.width = 60; 138 | _playButton.addEventListener(Event.TRIGGERED, onPlayButtonTriggered); 139 | container.addChild(_playButton); 140 | 141 | // Separator 142 | separator = new Quad(20, 1); 143 | separator.alpha = 0; 144 | container.addChild(separator); 145 | 146 | // unregister button 147 | _unregisterButton = new Button(); 148 | _unregisterButton.label = "X"; 149 | _unregisterButton.addEventListener(Event.TRIGGERED, onUnregisterButtonTriggered); 150 | container.addChild(_unregisterButton); 151 | } 152 | 153 | private function onBaseVolumeChanged():void 154 | { 155 | _trackConfiguration.baseVolume = _baseVolumeSlider.value; 156 | } 157 | 158 | private function onTrimButtonTriggered():void 159 | { 160 | _trackConfiguration.findTrim(); 161 | 162 | _trimStartTextInput.text = _trackConfiguration.trimStartDuration.toString(); // TODO silent! 163 | _trimEndTextInput.text = _trackConfiguration.trimEndDuration.toString(); // TODO silent! 164 | } 165 | 166 | private function onPlayButtonTriggered():void 167 | { 168 | _soundTest.playSound(_trackConfiguration.type, 0.5, parseInt(_startPositionTextInput.text), _loopStepper.value); 169 | } 170 | 171 | private function onUnregisterButtonTriggered():void 172 | { 173 | _soundTest.unregisterSound(_trackConfiguration); 174 | } 175 | 176 | private function onTrimStartDurationChanged(e:Event):void 177 | { 178 | try 179 | { 180 | _trackConfiguration.trimStartDuration = parseInt(_trimStartTextInput.text); 181 | } 182 | catch (e:ArgumentError) 183 | { 184 | trace("[Sound Configuration Entry] Cannot update trimming value. Error: " + e.message); 185 | } 186 | } 187 | 188 | private function onTrimEndDurationChanged(e:Event):void 189 | { 190 | try 191 | { 192 | _trackConfiguration.trimEndDuration = parseInt(_trimEndTextInput.text); 193 | } 194 | catch (e:ArgumentError) 195 | { 196 | trace("[Sound Configuration Entry] Cannot update trimming value. Error: " + e.message); 197 | } 198 | } 199 | 200 | public function get trackConfiguration():TrackConfiguration 201 | { 202 | return _trackConfiguration; 203 | } 204 | } 205 | } -------------------------------------------------------------------------------- /demo/src/ch/adolio/sound/TrackConfigurationEntryHeader.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import feathers.controls.Label; 14 | import feathers.controls.LayoutGroup; 15 | import feathers.layout.HorizontalLayout; 16 | import starling.core.Starling; 17 | import starling.display.Quad; 18 | import starling.display.Sprite; 19 | 20 | public class TrackConfigurationEntryHeader extends Sprite 21 | { 22 | public function TrackConfigurationEntryHeader() 23 | { 24 | // background 25 | var quad:Quad = new Quad(Starling.current.stage.stageWidth, 25, 0xffffff); 26 | quad.alpha = 0.8; 27 | addChild(quad); 28 | 29 | // horizontal layout 30 | var layout:HorizontalLayout = new HorizontalLayout(); 31 | layout.padding = 2; 32 | layout.gap = 4; 33 | var container:LayoutGroup = new LayoutGroup(); 34 | container.layout = layout; 35 | container.width = quad.width; 36 | container.height = quad.height; 37 | this.addChild(container); 38 | 39 | // sound label 40 | var soundTypeLabel:Label = new Label(); 41 | soundTypeLabel.width = 160; 42 | soundTypeLabel.text = "Type"; 43 | container.addChild(soundTypeLabel); 44 | 45 | // base volume 46 | var baseVolumeLabel:Label = new Label(); 47 | baseVolumeLabel.width = 80; 48 | baseVolumeLabel.text = "Base Volume"; 49 | container.addChild(baseVolumeLabel); 50 | 51 | // sound length in ms 52 | var soundLength:Label = new Label(); 53 | soundLength.width = 80; 54 | soundLength.text = "Duration (ms)"; 55 | container.addChild(soundLength); 56 | 57 | // Trim start label 58 | var trimStart:Label = new Label(); 59 | trimStart.width = 80; 60 | trimStart.text = "Trim start (ms)"; 61 | container.addChild(trimStart); 62 | 63 | // Trim end label 64 | var trimEnd:Label = new Label(); 65 | trimEnd.width = 80; 66 | trimEnd.text = "Trim end (ms)"; 67 | container.addChild(trimEnd); 68 | 69 | // Auto button 70 | var autoSeparator:Quad = new Quad(60, 1); 71 | autoSeparator.alpha = 0; 72 | container.addChild(autoSeparator); 73 | 74 | //----------------------------------------------------------------- 75 | //-- Controls 76 | //----------------------------------------------------------------- 77 | 78 | // Separator 79 | var separator:Quad = new Quad(20, 1); 80 | separator.alpha = 0; 81 | container.addChild(separator); 82 | 83 | // Loop label 84 | var loopLabel:Label = new Label(); 85 | loopLabel.text = "Loops"; 86 | loopLabel.width = 80; 87 | container.addChild(loopLabel); 88 | 89 | // Start position label 90 | var startPositionLabel:Label = new Label(); 91 | startPositionLabel.text = "Start pos (ms)"; 92 | startPositionLabel.width = 80; 93 | container.addChild(startPositionLabel); 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /libs/as3-signals.swc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Adolio/Syrinx-Sound-Manager/1f19c8c988390c4ea9e107fad765eb77b0ab612f/libs/as3-signals.swc -------------------------------------------------------------------------------- /src/ch/adolio/sound/Mp3Track.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.media.Sound; 14 | import flash.utils.ByteArray; 15 | 16 | /** 17 | * Native sound track (MP3) wrapper. 18 | * 19 | * @author Aurelien Da Campo 20 | */ 21 | public class Mp3Track extends ch.adolio.sound.Track 22 | { 23 | private var _nativeSound:flash.media.Sound; 24 | 25 | public function get sound():flash.media.Sound 26 | { 27 | return _nativeSound; 28 | } 29 | 30 | public function Mp3Track(nativeSound:flash.media.Sound) 31 | { 32 | _nativeSound = nativeSound; 33 | } 34 | 35 | public override function get length():Number 36 | { 37 | return _nativeSound.length; 38 | } 39 | 40 | public override function extract(target:ByteArray, length:Number, startPosition:Number = -1):Number 41 | { 42 | return _nativeSound.extract(target, length, startPosition); 43 | } 44 | 45 | public override function destroy():void 46 | { 47 | // Nullify references 48 | _nativeSound = null; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/SoundInstance.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.errors.IllegalOperationError; 14 | import flash.events.Event; 15 | import flash.events.EventDispatcher; 16 | import flash.events.SampleDataEvent; 17 | import flash.media.SoundChannel; 18 | import flash.media.SoundTransform; 19 | import flash.utils.ByteArray; 20 | import flash.utils.getTimer; 21 | import org.osflash.signals.Signal; 22 | 23 | /** 24 | * An instance of a sound. 25 | * 26 | * @author Aurelien Da Campo 27 | */ 28 | public class SoundInstance extends EventDispatcher 29 | { 30 | // Const 31 | public static const VOLUME_MIN:Number = 0; 32 | public static const VOLUME_MAX:Number = 1.0; 33 | public static const PITCH_DEFAULT:Number = 1.0; 34 | private static const SAMPLE_RATE_MS:Number = 44.1; // Frequency per millisecond 35 | private static const POSITION_EPSILON:Number = 0.001; // In milliseconds 36 | 37 | // Core 38 | private var _manager:SoundManager; 39 | private var _type:String; 40 | private var _track:Track; 41 | private var _channel:SoundChannel; 42 | private static var safeChannelAquisition:Boolean = true; // If false, an error will be raised on channel acquisition failure (play, resume, position set, etc.) 43 | private var _soundTransform:SoundTransform; 44 | private var _isStandardSound:Boolean; // Standard sound without any special effect such as trimming or pitching 45 | private var _standardSoundLoopPosition:Number = 0; // Used to keep track of the actual current position when looping 46 | 47 | // Options 48 | private var _infiniteLooping:Boolean = false; 49 | private var _loops:int = 0; 50 | private var _currentLoop:int = 0; 51 | private var _isMuted:Boolean = false; 52 | private var _baseVolume:Number = VOLUME_MAX; // base volume, not mixed 53 | private var _volume:Number = VOLUME_MAX; // instance volume, not mixed 54 | private var _pitch:Number = PITCH_DEFAULT; 55 | private var _isFromPool:Boolean = false; 56 | private var _freeWhenCompleted:Boolean = true; // Automatically destroy or release the instance when completed 57 | 58 | // Triming 59 | private var _trimStartDuration:int = 0; // In milliseconds 60 | private var _trimEndDuration:int = 0; // In milliseconds 61 | 62 | // Custom sampling 63 | private var _extractionFunc:Function; 64 | private var _fakeSound:flash.media.Sound; 65 | private var _readingTipPos:Number = 0; // The diamond position, in samples 66 | private var _samplingCount:uint = 0; 67 | private var _startPosition:uint; // In samples 68 | private var _endPosition:uint; // In samples 69 | private var _pitchExtractedData:ByteArray = new ByteArray(); 70 | private var _pitchStartPosition:Number = 0; // Position where the pitching started 71 | private var _pitchStartTimer:int = 0; // Used to track past time in pitching mode, in milliseconds 72 | 73 | // Status 74 | private var _isStarted:Boolean = false; 75 | private var _pauseTime:Number = 0; // In milliseconds 76 | private var _isPaused:Boolean = false; 77 | private var _isDestroyed:Boolean = false; 78 | 79 | // Signals 80 | public var started:Signal = new Signal(SoundInstance); // Dispatched when the sound is started (it happens only once) 81 | public var paused:Signal = new Signal(SoundInstance); // Dispatched when the sound is paused 82 | public var resumed:Signal = new Signal(SoundInstance); // Dispatched when the sound is resumed after pause 83 | public var stopped:Signal = new Signal(SoundInstance); // Dispatched when the sound is stopped 84 | public var completed:Signal = new Signal(SoundInstance); // Dispatched when the sound is completed 85 | public var destroyed:Signal = new Signal(SoundInstance); // Dispatched when the sound is destroyed 86 | 87 | // Debug 88 | private static const LOG_PREFIX:String = "[Sound Instance]"; 89 | private var _verbose:Boolean = false; 90 | private static var _idCount:uint = 0; 91 | private var _id:uint = 0; 92 | 93 | /** 94 | * Create a new sound instance from its track configuration and its manager. 95 | */ 96 | public function SoundInstance(trackConfig:TrackConfiguration, manager:SoundManager = null) 97 | { 98 | // Debug 99 | _id = _idCount++; 100 | 101 | // Setup fields from track configuration 102 | setupFromTrackConfiguration(trackConfig); 103 | 104 | // Setup sound transform 105 | _soundTransform = new SoundTransform(); 106 | 107 | // Setup custom sampling 108 | _extractionFunc = feedSamples; 109 | setupCustomSamplingVars(); 110 | _fakeSound = new flash.media.Sound(); 111 | _fakeSound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 112 | 113 | // Setup manager 114 | this.manager = manager; // This will trigger sound manager registration 115 | } 116 | 117 | private function setupFromTrackConfiguration(trackConfig:TrackConfiguration):void 118 | { 119 | // Check for argument validity 120 | if (!trackConfig) 121 | throw new ArgumentError(LOG_PREFIX + " Invalid argument. Track configuration is null."); 122 | 123 | // Setup core members 124 | _track = trackConfig.track; 125 | _type = trackConfig.type; 126 | 127 | // Setup track options 128 | _samplingCount = trackConfig.sampling; 129 | _baseVolume = trackConfig.baseVolume; 130 | _trimStartDuration = trackConfig.trimStartDuration; 131 | _trimEndDuration = trackConfig.trimEndDuration; 132 | } 133 | 134 | internal function setupFromPool(trackConfig:TrackConfiguration, manager:SoundManager = null):void 135 | { 136 | // Setup fields from track configuration 137 | setupFromTrackConfiguration(trackConfig); 138 | 139 | // Setup manager 140 | this.manager = manager; // This will trigger sound manager registration 141 | } 142 | 143 | internal function resetAfterRelease():void 144 | { 145 | // Remove from manager 146 | manager = null; 147 | 148 | // Reset internal status 149 | _loops = 0; 150 | _currentLoop = 0; 151 | _isMuted = false; 152 | _isPaused= false; 153 | _pitch = PITCH_DEFAULT; 154 | _isStarted = false; 155 | _pauseTime = 0; 156 | 157 | // Reset sound transform 158 | _soundTransform.volume = VOLUME_MAX; 159 | _soundTransform.pan = 0; 160 | 161 | // Clear signals 162 | clearSignals(false); 163 | } 164 | 165 | private function setupCustomSamplingVars():void 166 | { 167 | // Setup start & end positions 168 | _endPosition = (_track.length - _trimEndDuration) * SAMPLE_RATE_MS; 169 | _startPosition = _trimStartDuration * SAMPLE_RATE_MS; 170 | 171 | // Debug 172 | if (_verbose) 173 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' is setting up custom sampling vars. Start position: " + _startPosition + ", end position: " + _endPosition); 174 | } 175 | 176 | private function clearSignals(ignoreDestroyedSignal:Boolean):void 177 | { 178 | started.removeAll(); 179 | completed.removeAll(); 180 | paused.removeAll(); 181 | resumed.removeAll(); 182 | stopped.removeAll(); 183 | 184 | if (!ignoreDestroyedSignal) 185 | destroyed.removeAll(); 186 | } 187 | 188 | /** 189 | * Destroy sound instance. After this operation, the object should not be accessed anymore. 190 | */ 191 | public function destroy():void 192 | { 193 | // Cannot destroy twice 194 | if (_isDestroyed) 195 | return; 196 | 197 | // Mark object as destroyed 198 | _isDestroyed = true; 199 | 200 | // Reset signals (except the destroyed signal to dispatch it right after) 201 | clearSignals(true); 202 | 203 | // Unregister events 204 | if (_fakeSound) 205 | _fakeSound.removeEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData); 206 | 207 | // Stop channel 208 | stopChannel(); 209 | 210 | // Nullify references 211 | manager = null; // Will automatically unregister from manager 212 | _track = null; 213 | _soundTransform = null; 214 | _channel = null; 215 | _fakeSound = null; 216 | _pitchExtractedData = null; 217 | _extractionFunc = null; 218 | started = null; 219 | completed = null; 220 | stopped = null; 221 | paused = null; 222 | resumed = null; 223 | 224 | // Dispatch destroy event 225 | destroyed.dispatch(this); 226 | destroyed.removeAll(); 227 | destroyed = null; 228 | } 229 | 230 | /** 231 | * Indicates whether this sound instance is destroyed. 232 | * 233 | * A destroyed sound instance should not be accessed anymore. 234 | */ 235 | public function get isDestroyed():Boolean 236 | { 237 | return _isDestroyed; 238 | } 239 | 240 | //--------------------------------------------------------------------- 241 | //-- Sound Processing 242 | //--------------------------------------------------------------------- 243 | 244 | /** 245 | * Find next zero crossing position. 246 | * 247 | * This method checks both channels at the same time. 248 | */ 249 | private static function findZeroCrossingPosition(bytes:ByteArray, startPosition:uint = 0):uint 250 | { 251 | // Store bytes array initial position 252 | var initialPosition:uint = bytes.position; 253 | 254 | // Setup starting position 255 | bytes.position = startPosition; 256 | 257 | // Not enough bytes to read two float 258 | if (bytes.position + 8 >= bytes.length) 259 | return 0; 260 | 261 | // Setup 262 | var zeroCrossingPosition:uint = 0; 263 | var l0:Number = bytes.readFloat(); 264 | var r0:Number = bytes.readFloat(); 265 | 266 | // Look for zero-crossing for both side (stereo) 267 | while (bytes.position + 8 < bytes.length) 268 | { 269 | var l1:Number = bytes.readFloat(); 270 | var r1:Number = bytes.readFloat(); 271 | 272 | // Find zero crossing on both side 273 | if (l0 > 0 && l1 < 0 || l0 < 0 && l1 > 0 || l1 == 0 && r0 > 0 && r1 < 0 || r0 < 0 && r1 > 0 || r1 == 0) 274 | { 275 | zeroCrossingPosition = bytes.position; 276 | break; 277 | } 278 | } 279 | 280 | // Reset position 281 | bytes.position = initialPosition; 282 | return zeroCrossingPosition; 283 | } 284 | 285 | /** 286 | * Fill a given ByteArray with blank samples. 287 | */ 288 | private static function fillBlank(bytes:ByteArray, samples:int):void 289 | { 290 | for (var s:int = 0; s < samples; ++s) 291 | { 292 | bytes.writeFloat(0); 293 | bytes.writeFloat(0); 294 | } 295 | } 296 | 297 | /** 298 | * Data sampling handler. 299 | * 300 | *

301 | * According to AIR requirements, this method must fill between 2048 and 8192 samples (no less, no more). 302 | * The number of samples to fill in the event's data is given by `_samplingCount`. 303 | *

304 | * 305 | *

306 | * IMPORTANT FOR DEVS: 307 | *

    308 | *
  • Never dispatch events during data sampling.
  • 309 | *
  • Do not touch sound instance status variables during data sampling.
  • 310 | *
311 | *

312 | */ 313 | private function onSampleData(e:SampleDataEvent):void 314 | { 315 | var samplesFed:Number = 0; 316 | while (true) 317 | { 318 | // Extract samples & write them in the data output buffer 319 | samplesFed += _extractionFunc(e.data, _samplingCount - samplesFed); 320 | 321 | // Still looping & reached the end of the track? 322 | if ((_infiniteLooping || loopsRemaining > 0) && samplesFed < _samplingCount) 323 | { 324 | // Increase current loop 325 | _currentLoop++; 326 | 327 | // Reset the diamond 328 | _readingTipPos = _startPosition; 329 | 330 | // Continue reading... 331 | continue; 332 | } 333 | 334 | // Done 335 | break; 336 | } 337 | } 338 | 339 | /** 340 | * Simple feeding function without pitch support. 341 | * 342 | * It doesn't feed more data than it can. Looping operation should be handled by the caller. 343 | */ 344 | private function feedSamples(data:ByteArray, samplesToFeed:uint):Number 345 | { 346 | // Read maximum number of bytes from source 347 | var samplesToRead:Number = Math.min(_endPosition - _readingTipPos, samplesToFeed); 348 | 349 | // Extract & feed data 350 | var samplesRead:Number = _track.extract(data, samplesToRead, _readingTipPos); 351 | 352 | // Update reading position 353 | _readingTipPos += samplesRead; 354 | 355 | // Return number of written samples 356 | return samplesRead; 357 | } 358 | 359 | /** 360 | * Feeding function which supports pitching capability. 361 | * 362 | * It doesn't feed more data than it can. Looping operation should be handled by the caller. 363 | * 364 | * This method has been inspired by Andre Michelle's implementation found here: http://blog.andre-michelle.com/2009/pitch-mp3/ 365 | */ 366 | private function feedPitchedSamples(data:ByteArray, samplesToFeed:uint):Number 367 | { 368 | // Reuse byte array instead of recreation 369 | _pitchExtractedData.position = 0; 370 | 371 | // Setup core variables 372 | var scaledBlockSize:Number = samplesToFeed * _pitch; 373 | var positionInt:int = _readingTipPos; 374 | var alpha:Number = _readingTipPos - positionInt; 375 | var positionTargetNum:Number = alpha; 376 | var positionTargetInt:int = -1; 377 | 378 | // Compute number of samples needed to process block (+2 for interpolation) 379 | var samplesNeeded:int = Math.ceil(scaledBlockSize) + 2; 380 | 381 | // Extract samples 382 | var samplesRead:int = _track.extract(_pitchExtractedData, samplesNeeded, positionInt); 383 | var samplesToWrite:int = samplesRead == samplesNeeded ? samplesToFeed : samplesRead / _pitch; 384 | 385 | // For all samples required to be written 386 | var l0:Number; 387 | var r0:Number; 388 | var l1:Number; 389 | var r1:Number; 390 | for (var i:int = 0 ; i < samplesToWrite ; ++i) 391 | { 392 | // Avoid reading equal samples, if rate < 1.0 393 | if (int(positionTargetNum) != positionTargetInt) 394 | { 395 | positionTargetInt = positionTargetNum; 396 | 397 | // Set target read position 398 | _pitchExtractedData.position = positionTargetInt << 3; 399 | 400 | // Read two stereo samples for linear interpolation 401 | l0 = _pitchExtractedData.readFloat(); 402 | r0 = _pitchExtractedData.readFloat(); 403 | l1 = _pitchExtractedData.readFloat(); 404 | r1 = _pitchExtractedData.readFloat(); 405 | } 406 | 407 | // Write interpolated amplitude into the stream 408 | data.writeFloat(l0 + alpha * (l1 - l0)); 409 | data.writeFloat(r0 + alpha * (r1 - r0)); 410 | 411 | // Increase the target position 412 | positionTargetNum += _pitch; 413 | 414 | // Increase fraction and keep only the fractional part 415 | alpha += _pitch; 416 | alpha -= int(alpha); 417 | } 418 | 419 | // Update reading tip position 420 | _readingTipPos += scaledBlockSize; 421 | 422 | // Return number of written samples 423 | return samplesToWrite; 424 | } 425 | 426 | //--------------------------------------------------------------------- 427 | //-- Sound Control 428 | //--------------------------------------------------------------------- 429 | 430 | /** 431 | * Play the sound instance. 432 | * 433 | * @param volume The initial volume 434 | * @param startTime Start position in milliseconds 435 | * @param loops Number of sound repetitions. -1 for infinite looping. 436 | */ 437 | public function play(volume:Number = VOLUME_MAX, startTime:Number = 0, loops:int = 0):SoundInstance 438 | { 439 | // setup looping properties 440 | _loops = loops; 441 | _infiniteLooping = loops < 0; 442 | 443 | // setup looping properties from start time 444 | setupLoopingPropertiesFromPosition(startTime); 445 | 446 | // play 447 | return playInternal(volume, startTime % length); 448 | } 449 | 450 | /** 451 | * Internal play. 452 | * 453 | * Beware: `startTime` should NOT be greater than the sound `length`! 454 | */ 455 | private function playInternal(volume:Number = VOLUME_MAX, startTime:Number = 0, startPaused:Boolean = false):SoundInstance 456 | { 457 | // Stop existing channel 458 | if (_channel) 459 | { 460 | stopChannel(); 461 | _channel = null; 462 | } 463 | 464 | // Check for position validity 465 | if (startTime < 0 || startTime > length) 466 | throw new ArgumentError(LOG_PREFIX + " Invalid position. Start time must be in the following interval [0.." + length + "]. Start time: " + startTime); 467 | 468 | // compute position in total length 469 | var positionInTotalLength:Number = _currentLoop * length + startTime; 470 | 471 | // Direct end case. This prevents to play an extra repetition when playing a sound exactly at the end of it 472 | if (approximately(positionInTotalLength, totalLength, POSITION_EPSILON)) 473 | { 474 | complete(); 475 | return this; 476 | } 477 | 478 | // Reset pause / resume status 479 | _isPaused = startPaused; 480 | _pauseTime = startPaused ? positionInTotalLength : 0; 481 | 482 | // Detect if the sound is a standard one (native MP3, no trimming & no pitch) 483 | _isStandardSound = _track is Mp3Track && _pitch == PITCH_DEFAULT && _trimStartDuration == 0 && _trimEndDuration == 0; 484 | 485 | // Setup non-standard sound 486 | if (!_isStandardSound) 487 | { 488 | _readingTipPos = _startPosition + int(startTime * SAMPLE_RATE_MS); // Setup the diamond 489 | pitchStartPosition = positionInTotalLength; // Pitch setup in case pitch is set 490 | } 491 | 492 | // Acquire a channel if sound doesn't start paused 493 | if (!startPaused) 494 | { 495 | // Capacity check if Sound Manager available 496 | if (manager == null || manager.getPlayingSoundInstancesCount() < manager.maxChannelCapacity) 497 | { 498 | // Acquire a sound channel 499 | if (_isStandardSound) 500 | _channel = (_track as Mp3Track).sound.play(startTime, 0, _isMuted ? new SoundTransform(0) : _soundTransform); 501 | else 502 | _channel = _fakeSound.play(startTime, -1, _isMuted ? new SoundTransform(0) : _soundTransform); 503 | } 504 | else 505 | { 506 | _channel = null; 507 | } 508 | 509 | // Channel can be null if maximum number of sound channels available is reached or if no sound card is available. 510 | if (_channel == null) 511 | { 512 | var errorMessage:String = LOG_PREFIX + " Impossible to acquire a sound channel for sound '" + _type + "#" + _id + "'. The maximum number of channels has been reached or there is no sound card available."; 513 | if (safeChannelAquisition) 514 | trace(errorMessage); 515 | else 516 | throw new IllegalOperationError(errorMessage); 517 | 518 | // Put the sound in hold 519 | _isPaused = true; 520 | } 521 | else 522 | { 523 | // Listen to sound completion 524 | _channel.addEventListener(Event.SOUND_COMPLETE, onSoundComplete); 525 | } 526 | 527 | // Set volume (+check) after having created the channel 528 | this.volume = volume; 529 | } 530 | 531 | // Sound started (only once) 532 | if (!_isStarted) 533 | { 534 | _isStarted = true; 535 | started.dispatch(this); 536 | } 537 | 538 | return this; 539 | } 540 | 541 | /** 542 | * Stop the sound. 543 | * 544 | * Stopped event will then be dispatched. 545 | */ 546 | public function stop():void 547 | { 548 | // Debug 549 | if (_verbose) 550 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' stopped."); 551 | 552 | // Stop channel if playing 553 | if (_channel) 554 | { 555 | stopChannel(); 556 | _channel = null; 557 | } 558 | 559 | // Reset status 560 | _isStarted = false; 561 | _pauseTime = 0; 562 | _currentLoop = 0; 563 | _isPaused = false; 564 | 565 | // Dispatch stopped event 566 | stopped.dispatch(this); 567 | } 568 | 569 | /** 570 | * Complete the sound. 571 | * 572 | * Completed event will then be dispatched. 573 | */ 574 | private function complete():void 575 | { 576 | // Debug 577 | if (_verbose) 578 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' complete."); 579 | 580 | // Dispatch completion event 581 | completed.dispatch(this); 582 | } 583 | 584 | /** 585 | * Get muting status of current sound. 586 | */ 587 | public function get isMuted():Boolean 588 | { 589 | return _isMuted; 590 | } 591 | 592 | /** 593 | * Mute / unmute current sound. 594 | * 595 | * - A muted sound continues to play. 596 | * - Volume modifications of a muted sound won't unmute the sound but will be kept. 597 | * - Panning modifications of a muted sound won't unmute the sound but will be kept. 598 | */ 599 | public function set isMuted(value:Boolean):void 600 | { 601 | // Update muting status 602 | _isMuted = value; 603 | 604 | // Update channel sound transform if any 605 | if (_channel != null) 606 | _channel.soundTransform = _isMuted ? new SoundTransform(0, _soundTransform.pan) : _soundTransform; 607 | } 608 | 609 | /** 610 | * Pause currently playing sound. 611 | * 612 | * Use resume() to continue playback. Pause / resume is supported for a single sound only. 613 | */ 614 | public function pause():SoundInstance 615 | { 616 | // Check for channel validity 617 | if (_channel == null) 618 | return this; 619 | 620 | // Pause 621 | _pauseTime = position; 622 | _isPaused = true; 623 | 624 | // stop the channel 625 | stopChannel(); 626 | _channel = null; 627 | 628 | // Debug 629 | if (_verbose) 630 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' paused. Pause time:" + _pauseTime); 631 | 632 | // Dispatch event 633 | paused.dispatch(this); 634 | 635 | // Return instance for chaining 636 | return this; 637 | } 638 | 639 | /** 640 | * Resume a paused sound. 641 | */ 642 | public function resume():SoundInstance 643 | { 644 | // Already running... 645 | if (!_isPaused) 646 | return this; 647 | 648 | // Resume 649 | _isPaused = false; 650 | 651 | // Play at current pause position 652 | playInternal(volume, _pauseTime % length); 653 | 654 | // Beware: play can trigger completion right away & destruction in the time! 655 | if (_isDestroyed) 656 | return this; 657 | 658 | // Debug 659 | if (_verbose) 660 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' resumed. Resume time:" + _pauseTime); 661 | 662 | // Dispatch event 663 | resumed.dispatch(this); 664 | 665 | // Return instance for chaining 666 | return this; 667 | } 668 | 669 | /** 670 | * Indicates whether this sound is currently paused. 671 | */ 672 | public function get isPaused():Boolean 673 | { 674 | return _isPaused; 675 | } 676 | 677 | /** 678 | * Set pausing status. 679 | */ 680 | public function set isPaused(value:Boolean):void 681 | { 682 | if (value) 683 | pause(); 684 | else 685 | resume(); 686 | } 687 | 688 | /** 689 | * Indicates whether this sound is currently playing. 690 | */ 691 | public function get isPlaying():Boolean 692 | { 693 | return _channel != null; 694 | } 695 | 696 | /** 697 | * Indicates whether this sound instance has started (play() method has been called and succeeded once). 698 | */ 699 | public function get isStarted():Boolean 700 | { 701 | return _isStarted; 702 | } 703 | 704 | /** 705 | * Get the actual length of the sound in milliseconds with trimming options. 706 | * 707 | * Note: This doesn't take into account loops. For length with looping options, have a look to `totalLength` method. 708 | */ 709 | public function get length():Number 710 | { 711 | return _track.length - (_trimStartDuration + _trimEndDuration); 712 | } 713 | 714 | /** 715 | * Get the total length of the sound in milliseconds with trimming & looping options. 716 | * 717 | * Note: If infinite looping is active, `Number.POSITIVE_INFINITY` will be returned. 718 | */ 719 | public function get totalLength():Number 720 | { 721 | if (_infiniteLooping) 722 | return Number.POSITIVE_INFINITY; 723 | else 724 | return length * (_loops + 1); 725 | } 726 | 727 | /** 728 | * Returns the remaining play time in milliseconds. 729 | * 730 | * Note: If infinite looping is active, `Number.POSITIVE_INFINITY` will be returned. 731 | */ 732 | public function get remainingTime():Number 733 | { 734 | if (_infiniteLooping) 735 | return Number.POSITIVE_INFINITY; 736 | else 737 | return (length - position) + loopsRemaining * length; 738 | } 739 | 740 | /** 741 | * Position ratio in the current loop. 742 | * 743 | * Each loop start at 0 and ends at 1.0. 744 | */ 745 | public function get positionRatioInLoop():Number 746 | { 747 | // Prevent potential devision by zero 748 | var length:Number = this.length; 749 | if (length <= 0) 750 | return 0; 751 | 752 | // Compute & return the local ratio 753 | return (position % length) / length; 754 | } 755 | 756 | /** 757 | * Set the normalized position of the sound (between 0..1). 758 | * 759 | * Note: Loops are part of the equation. 760 | * Infinite looping sound will always return 0. 761 | */ 762 | public function get positionRatio():Number 763 | { 764 | // Prevent potential devision by zero 765 | var totalLength:Number = this.totalLength; 766 | if (totalLength <= 0) 767 | return 0; 768 | 769 | // Compute & return the ratio 770 | return position / totalLength; 771 | } 772 | 773 | /** 774 | * Set position with normalized position of sound. 775 | * 776 | * Note: Loops are part of the equation. 777 | */ 778 | public function set positionRatio(value:Number):void 779 | { 780 | // Check for position ratio validity 781 | if (value < 0 || value > 1.0) 782 | throw new ArgumentError(LOG_PREFIX + " Invalid position ratio. Value must be in the following interval [0..1.0]"); 783 | 784 | // Debug 785 | if (_verbose) 786 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' position ratio changed to:" + value); 787 | 788 | // Update the position based on the total length 789 | position = value * totalLength; 790 | } 791 | 792 | /** 793 | * Get the current position of the sound in milliseconds. 794 | * 795 | * This is the actual position in the track (non-pitched). 796 | */ 797 | public function get position():Number 798 | { 799 | // Not started yet or paused 800 | if (_channel == null) 801 | return _pauseTime; 802 | 803 | // Standard sound case 804 | if (_isStandardSound) 805 | return _channel.position + _standardSoundLoopPosition; 806 | 807 | // When the pitch parameter is set, the past time is influenced by the pitch 808 | if (_pitch != PITCH_DEFAULT) 809 | return _pitchStartPosition + (getTimer() - _pitchStartTimer) * _pitch; 810 | 811 | // Playing sound 812 | return _channel.position; 813 | } 814 | 815 | /** 816 | * Set the current position of the sound in milliseconds. 817 | */ 818 | public function set position(value:Number):void 819 | { 820 | // Check for position validity 821 | if (value < 0 || value > totalLength) 822 | throw new ArgumentError(LOG_PREFIX + " Invalid position. Value must be in the following interval [0.." + totalLength + "]. Value: " + value); 823 | 824 | // Update paused sound 825 | if (_isPaused || !_isStarted) 826 | _pauseTime = value; 827 | 828 | // update looping properties 829 | setupLoopingPropertiesFromPosition(value); 830 | 831 | // Update playing sound 832 | if (_channel != null) 833 | { 834 | // Stop the channel 835 | stopChannel(); 836 | 837 | // Play the sound at the new position with the same parameters 838 | playInternal(volume, value % length, isPaused); 839 | } 840 | 841 | // Debug 842 | if (_verbose) 843 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' position changed to " + value + " ms."); 844 | } 845 | 846 | /** 847 | * Get the base volume. 848 | * 849 | * Returns a value between 0 and 1. 850 | */ 851 | public function get baseVolume():Number 852 | { 853 | return _baseVolume; 854 | } 855 | 856 | /** 857 | * Set the base volume. 858 | * 859 | *

The base volume is here mainly for sound mastering purposes. 860 | * It allows to play around with the `volume` and keep a pre-configured "base" volume.

861 | * 862 | *

This value should be configured per track in the `TrackConfiguration` and shouldn't be touched here.

863 | * 864 | * @see mixedVolume 865 | * @see TrackConfiguration 866 | */ 867 | public function set baseVolume(value:Number):void 868 | { 869 | // Update the voume value, but respect the mute flag. 870 | if (value < VOLUME_MIN) 871 | value = VOLUME_MIN; 872 | else if (value > VOLUME_MAX || isNaN(volume)) 873 | value = VOLUME_MAX; 874 | 875 | // Set internal base volume 876 | _baseVolume = value; 877 | 878 | // Update current sound transform 879 | _soundTransform.volume = mixedVolume; 880 | 881 | // Debug 882 | if (_verbose) 883 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' base volume updated to " + value + ". Mixed volume: " + mixedVolume); 884 | 885 | // Apply sound transform if possible & not muted 886 | if (!_isMuted && _channel) 887 | _channel.soundTransform = _soundTransform; 888 | } 889 | 890 | /** 891 | * Get the volume. 892 | * 893 | * Returns a value between 0 and 1. 894 | */ 895 | public function get volume():Number 896 | { 897 | return _volume; 898 | } 899 | 900 | /** 901 | * Set the volume. 902 | * 903 | * The final volume is mixed with the sound manager volume. 904 | * 905 | * @see mixedVolume 906 | */ 907 | public function set volume(value:Number):void 908 | { 909 | // Update the voume value, but respect the mute flag. 910 | if (value < VOLUME_MIN) 911 | value = VOLUME_MIN; 912 | else if (value > VOLUME_MAX || isNaN(volume)) 913 | value = VOLUME_MAX; 914 | 915 | // Set internal volume 916 | _volume = value; 917 | 918 | // Update current sound transform 919 | _soundTransform.volume = mixedVolume; 920 | 921 | // Debug 922 | if (_verbose) 923 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' volume updated to " + value + ". Mixed volume: " + mixedVolume); 924 | 925 | // Apply sound transform if possible & not muted 926 | if (!_isMuted && _channel) 927 | _channel.soundTransform = _soundTransform; 928 | } 929 | 930 | /** 931 | * Return the combined manager volume, base volume and volume. 932 | * 933 | *
mixedVolume = volume * baseVolume * manager.volume
934 | */ 935 | public function get mixedVolume():Number 936 | { 937 | return _volume * _baseVolume * (_manager ? _manager.volume : 1.0); 938 | } 939 | 940 | /** 941 | * Get the left-to-right panning of the sound, ranging from -1 (full pan left) to 1 (full pan right). 942 | */ 943 | public function get pan():Number 944 | { 945 | return _soundTransform.pan; 946 | } 947 | 948 | /** 949 | * Set the left-to-right panning of the sound, ranging from -1 (full pan left) to 1 (full pan right). 950 | */ 951 | public function set pan(value:Number):void 952 | { 953 | // Clamp panning from -1.0 to 1.0 954 | value = Math.max(-1.0, Math.min(value, 1.0)); 955 | 956 | // Debug 957 | if (_verbose) 958 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' pan set to " + value + "."); 959 | 960 | // Update pan 961 | _soundTransform.pan = value; 962 | 963 | // Apply sound transform if possible & not muted 964 | if (!_isMuted && _channel) 965 | _channel.soundTransform = _soundTransform; 966 | } 967 | 968 | /** 969 | * Get the pitch. 970 | */ 971 | public function get pitch():Number 972 | { 973 | return _pitch; 974 | } 975 | 976 | /** 977 | * Set the pitch. 978 | */ 979 | public function set pitch(value:Number):void 980 | { 981 | // Debug 982 | if (_verbose) 983 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' pitch set to " + value + "."); 984 | 985 | // Update pitch start position with the current position 986 | if (_infiniteLooping) 987 | pitchStartPosition = positionRatioInLoop; 988 | else 989 | pitchStartPosition = position; 990 | 991 | // Update pitch 992 | _pitch = value; 993 | 994 | // Assign the right extraction function according to pitching value 995 | _extractionFunc = _pitch == PITCH_DEFAULT ? feedSamples : feedPitchedSamples; 996 | 997 | // Leave standard sound 998 | if (_isStandardSound && _pitch != PITCH_DEFAULT) 999 | { 1000 | // Debug 1001 | if (_verbose) 1002 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' is not anymore a standard sound."); 1003 | 1004 | // Restart sound with proper configuration 1005 | setupLoopingPropertiesFromPosition(position); 1006 | playInternal(volume, position % length, isPaused); 1007 | } 1008 | } 1009 | 1010 | /** 1011 | * Used to update start pitching position & therefore reset the pitch start time 1012 | */ 1013 | private function set pitchStartPosition(position:Number):void 1014 | { 1015 | // Update position 1016 | _pitchStartPosition = position; 1017 | 1018 | // Important: Reset real-time stopwatch 1019 | _pitchStartTimer = getTimer(); 1020 | } 1021 | 1022 | /** 1023 | * Get the Sound Manager. 1024 | */ 1025 | public function get manager():SoundManager 1026 | { 1027 | return _manager; 1028 | } 1029 | 1030 | /** 1031 | * Set the Sound Manager. 1032 | * 1033 | * This will automatically trigger unregistration from the previous Sound Manager & registration to the new one. 1034 | */ 1035 | public function set manager(value:SoundManager):void 1036 | { 1037 | // Remove sound from previous sound manager 1038 | if (_manager) 1039 | _manager.removeSoundInstance(this); 1040 | 1041 | // Update sound manager 1042 | _manager = value; 1043 | 1044 | // Add sound in the new sound manager 1045 | if (_manager) 1046 | _manager.addSoundInstance(this); 1047 | 1048 | // Debug 1049 | if (_verbose) 1050 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' manager changed to " + value + "."); 1051 | 1052 | // Update (mixed) volume 1053 | this.volume = volume; 1054 | } 1055 | 1056 | /** 1057 | * Return the total loops or -1 if looping is infinite. 1058 | */ 1059 | public function get loops():int 1060 | { 1061 | return _infiniteLooping ? -1 : _loops; 1062 | } 1063 | 1064 | public function get currentLoop():int 1065 | { 1066 | return _currentLoop; 1067 | } 1068 | 1069 | /** 1070 | * Return the remaining loops or -1 if looping is infinite. 1071 | */ 1072 | public function get loopsRemaining():int 1073 | { 1074 | // Infinite looping case 1075 | if (_infiniteLooping) 1076 | return -1; 1077 | 1078 | // Compute remaining loops based on the actual channel position 1079 | return _loops - _currentLoop; 1080 | } 1081 | 1082 | /** 1083 | * Setup the looping properties from a given position. 1084 | * 1085 | * This must be done before playing a sound internally since the `playInternal` method works only on the sound length. 1086 | */ 1087 | private function setupLoopingPropertiesFromPosition(position:Number):void 1088 | { 1089 | _currentLoop = Math.floor(position / length); 1090 | _standardSoundLoopPosition = _currentLoop * length; 1091 | } 1092 | 1093 | /** 1094 | * Type 1095 | */ 1096 | public function get type():String 1097 | { 1098 | return _type; 1099 | } 1100 | 1101 | /** 1102 | * Is the sound instance coming from a pool? 1103 | */ 1104 | public function get isFromPool():Boolean 1105 | { 1106 | return _isFromPool; 1107 | } 1108 | 1109 | /** 1110 | * Is the sound instance coming from a pool? 1111 | */ 1112 | internal function get fromPool():Boolean 1113 | { 1114 | return _isFromPool; 1115 | } 1116 | 1117 | /** 1118 | * Sets the source location of the sound instance 1119 | */ 1120 | internal function set fromPool(value:Boolean):void 1121 | { 1122 | _isFromPool = value; 1123 | } 1124 | 1125 | /** 1126 | * Gets the free when completed flags. 1127 | */ 1128 | public function get freeWhenCompleted():Boolean 1129 | { 1130 | return _freeWhenCompleted; 1131 | } 1132 | 1133 | /** 1134 | * Sets the free when completed flags. 1135 | */ 1136 | public function set freeWhenCompleted(value:Boolean):void 1137 | { 1138 | _freeWhenCompleted = value; 1139 | } 1140 | 1141 | /** 1142 | * Get verbose mode activity status. 1143 | */ 1144 | public function get verbose():Boolean 1145 | { 1146 | return _verbose; 1147 | } 1148 | 1149 | /** 1150 | * Set verbose mode activity. 1151 | */ 1152 | public function set verbose(value:Boolean):void 1153 | { 1154 | _verbose = value; 1155 | } 1156 | 1157 | /** 1158 | * Get the sound instance unique id. 1159 | */ 1160 | public function get id():uint 1161 | { 1162 | return _id; 1163 | } 1164 | 1165 | //--------------------------------------------------------------------- 1166 | //-- Internal 1167 | //--------------------------------------------------------------------- 1168 | 1169 | /** 1170 | * Stop the currently playing channel. 1171 | */ 1172 | private function stopChannel():void 1173 | { 1174 | // Check if channed is valid 1175 | if (!_channel) 1176 | return; 1177 | 1178 | // Unregister from complete event 1179 | _channel.removeEventListener(Event.SOUND_COMPLETE, onSoundComplete); 1180 | 1181 | // Attempt to stop the channel 1182 | try 1183 | { 1184 | _channel.stop(); 1185 | } 1186 | catch (e:Error) 1187 | { 1188 | trace(LOG_PREFIX + " Impossible to stop the channel. Error: " + e.message); 1189 | } 1190 | } 1191 | 1192 | //--------------------------------------------------------------------- 1193 | //-- Utility 1194 | //--------------------------------------------------------------------- 1195 | 1196 | private static function approximately(a:Number, b:Number, epsilon:Number):Boolean 1197 | { 1198 | return Math.abs(b - a) <= epsilon; 1199 | } 1200 | 1201 | //--------------------------------------------------------------------- 1202 | //-- Event handlers 1203 | //--------------------------------------------------------------------- 1204 | 1205 | /** 1206 | * On sound complete. 1207 | */ 1208 | private function onSoundComplete(e:Event):void 1209 | { 1210 | if (_verbose) 1211 | trace(LOG_PREFIX + " Sound '" + _type + "#" + _id + "' completed."); 1212 | 1213 | // Handle looping for standard sound 1214 | if (_isStandardSound) 1215 | { 1216 | _currentLoop++; 1217 | 1218 | // HACK because _channel.position is not always exactly equals to length on sound completed. 1219 | // Stop the channel & update pause time to get the right position when requesting it. 1220 | stopChannel(); 1221 | _channel = null; 1222 | _pauseTime = _standardSoundLoopPosition = _currentLoop * length; // update loop starting position 1223 | 1224 | // Debug 1225 | if (_verbose) 1226 | trace(LOG_PREFIX + " Standard sound completed. Current loop: " + _currentLoop + ", Loops remaining: " + loopsRemaining); 1227 | 1228 | // Replay? 1229 | if (_infiniteLooping || loopsRemaining >= 0) 1230 | { 1231 | // Continue playing... 1232 | playInternal(volume); 1233 | return; 1234 | } 1235 | } 1236 | 1237 | // Complete the sound 1238 | complete(); 1239 | } 1240 | } 1241 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/SoundInstancesPool.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | /** 14 | * Sound instances pool. 15 | * 16 | * @author Aurelien Da Campo 17 | */ 18 | public class SoundInstancesPool 19 | { 20 | // Pool 21 | private var _pool:Vector. = new Vector.(); 22 | 23 | // Singleton 24 | private static var _instance:SoundInstancesPool; 25 | 26 | // Stats 27 | private var _acquiredCount:uint = 0; 28 | private var _savedCount:uint = 0; 29 | private var _releaseCount:uint = 0; 30 | 31 | // Debug 32 | private static const LOG_PREFIX:String = "[Sound Instances Pool]"; 33 | private var _verbose:Boolean = false; 34 | 35 | /** 36 | * Singleton 37 | */ 38 | public static function get instance():SoundInstancesPool 39 | { 40 | if (!_instance) 41 | _instance = new SoundInstancesPool(); 42 | 43 | return _instance; 44 | } 45 | 46 | /** 47 | * Acquires a Sound Instance. 48 | * 49 | * Don't forget the release it afterwards. 50 | * 51 | * @param trackConfig The track configuration 52 | * @param manager The manager to which the sound instance should be added 53 | * @return a Sound Instance found from the pool or newly created 54 | */ 55 | public function acquireSound(trackConfig:TrackConfiguration, manager:SoundManager = null):SoundInstance 56 | { 57 | var sound:SoundInstance; 58 | 59 | ++_acquiredCount; 60 | 61 | // Look in the pool 62 | var soundCount:int = _pool.length; 63 | for (var i:int = 0; i < soundCount; ++i) 64 | { 65 | sound = _pool[i]; 66 | if (sound.type == trackConfig.type) 67 | { 68 | ++_savedCount; 69 | 70 | if (_verbose) 71 | trace(LOG_PREFIX + " Sound Instance acquired from pool. Saved instantiations: " + _savedCount); 72 | 73 | sound.setupFromPool(trackConfig, manager); 74 | return _pool.removeAt(i); 75 | } 76 | } 77 | 78 | // Create new one 79 | if (_verbose) 80 | trace(LOG_PREFIX + " Sound Instance created from pool."); 81 | 82 | sound = new SoundInstance(trackConfig, manager); 83 | sound.fromPool = true; // Mark as coming from the pool to restore it later on 84 | return sound; 85 | } 86 | 87 | /** 88 | * Releases a Sound Instance. 89 | * 90 | * @param sound The sound instance to release 91 | */ 92 | public function releaseSound(sound:SoundInstance):void 93 | { 94 | // Check for `null` sound instance 95 | if (!sound) 96 | throw ArgumentError("A `null` sound instance cannot be released"); 97 | 98 | // Check for invalid sound instance 99 | if (sound.isDestroyed) 100 | throw ArgumentError("A destroyed sound instance cannot be released."); 101 | 102 | ++_releaseCount; 103 | 104 | // Stop the sound 105 | sound.stop(); 106 | 107 | // Reset & add back to the pool 108 | sound.resetAfterRelease(); 109 | _pool.push(sound); 110 | 111 | // Debug 112 | if (_verbose) 113 | trace(LOG_PREFIX + " Sound Instance released to pool."); 114 | } 115 | 116 | public function get capacity():uint 117 | { 118 | return _pool.length; 119 | } 120 | 121 | public function get acquiredCount():uint 122 | { 123 | return _acquiredCount; 124 | } 125 | 126 | public function get savedCount():uint 127 | { 128 | return _savedCount; 129 | } 130 | 131 | public function get releaseCount():uint 132 | { 133 | return _releaseCount; 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/SoundManager.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.errors.IllegalOperationError; 14 | import flash.utils.Dictionary; 15 | import org.osflash.signals.Signal; 16 | 17 | /** 18 | * Sound Manager. 19 | * 20 | *

The Sound Manager allows to play sounds & handles sound instances.

21 | * 22 | * @author Aurelien Da Campo 23 | */ 24 | public class SoundManager 25 | { 26 | // Const 27 | public static const VERSION:String = "0.6"; 28 | public static const MAX_CHANNELS:uint = 32; // Adobe AIR hard limit 29 | 30 | // Core 31 | private var _tracksByType:Dictionary = new Dictionary(); // Dictionary of tracks 32 | private var _soundInstances:Vector. = new Vector.(); 33 | private var _maxChannelCapacity:uint = MAX_CHANNELS; // Maximum number of sound instances playing simultaneously. The hard limit from Adobe AIR is `MAX_CHANNELS` for all Sound Managers together. 34 | 35 | // Options 36 | private var _volume:Number = 1.0; 37 | private var _isPoolingEnabled:Boolean = false; 38 | 39 | // Signals 40 | public var trackRegistered:Signal = new Signal(TrackConfiguration); 41 | public var trackUnregistered:Signal = new Signal(TrackConfiguration); 42 | public var soundInstanceAdded:Signal = new Signal(SoundInstance); 43 | public var soundInstanceRemoved:Signal = new Signal(SoundInstance); 44 | public var soundInstanceReleasedToPool:Signal = new Signal(SoundInstance, SoundInstancesPool); // Dispatched when a sound instance is released to the pool 45 | 46 | // Debug 47 | private static const LOG_PREFIX:String = "[Sound Manager]"; 48 | 49 | //--------------------------------------------------------------------- 50 | //-- Sound Instance Management 51 | //--------------------------------------------------------------------- 52 | 53 | /** 54 | * Play a sound of a given registered type. 55 | * 56 | * @param type The type 57 | * @param volume Volume of the sound at start 58 | * @param startTime Time at which the sound will start (in ms) 59 | * @param loops Number of loops of the sound (0 = no loop, -1 = infinite looping) 60 | * @return The sound instance of the newly created sound. null if no sound has been found. 61 | */ 62 | public function play(type:String, volume:Number = 1.0, startTime:Number = 0, loops:int = 0):SoundInstance 63 | { 64 | // Create the sound instance 65 | var si:SoundInstance = createSound(type); 66 | 67 | // Start playing 68 | si.play(volume, startTime, loops); 69 | 70 | return si; 71 | } 72 | 73 | /** 74 | * Request sound instances by type attached to this manager. 75 | */ 76 | public function getSoundInstancesByType(type:String):Vector. 77 | { 78 | // Setup output list 79 | var instances:Vector. = new Vector.(); 80 | 81 | // Look for all instances of the given type 82 | var si:SoundInstance; 83 | var length:int = _soundInstances.length; 84 | for (var i:int = 0; i < length; ++i) 85 | { 86 | si = _soundInstances[i]; 87 | if (si.type == type) 88 | instances.push(si); 89 | } 90 | 91 | return instances; 92 | } 93 | 94 | /** 95 | * Return the number of current sound instances. 96 | */ 97 | public function getSoundInstancesCount():uint 98 | { 99 | return _soundInstances.length; 100 | } 101 | 102 | /** 103 | * Return the playing number of current sound instances. 104 | */ 105 | public function getPlayingSoundInstancesCount():uint 106 | { 107 | var count:uint = 0; 108 | var length:int = _soundInstances.length; 109 | for (var i:int = 0; i < length; ++i) 110 | if (_soundInstances[i].isPlaying) 111 | count++; 112 | 113 | return count; 114 | } 115 | 116 | /** 117 | * Check if the sound manager has a sound instance of the given type. 118 | */ 119 | public function hasSoundInstancesOfType(type:String):Boolean 120 | { 121 | // Look for any instances of the given type 122 | var length:int = _soundInstances.length; 123 | for (var i:int = 0; i < length; ++i) 124 | if (_soundInstances[i].type == type) 125 | return true; 126 | 127 | return false; 128 | } 129 | 130 | public function getSoundInstances():Vector. 131 | { 132 | // Setup output list 133 | var instances:Vector. = new Vector.(); 134 | 135 | var length:int = _soundInstances.length; 136 | for (var i:int = 0; i < length; ++i) 137 | instances.push(_soundInstances[i]); 138 | 139 | return instances; 140 | } 141 | 142 | /** 143 | * Stop all sounds immediately. 144 | */ 145 | public function stopAll():void 146 | { 147 | var length:int = _soundInstances.length; 148 | for (var i:int = 0; i < length; ++i) 149 | _soundInstances[i].stop(); 150 | } 151 | 152 | /** 153 | * Destroy all sound instances. 154 | * 155 | * Sounds can be stopped before destruction if required. 156 | */ 157 | public function destroyAllSoundInstances(stopSoundBeforeDestroying:Boolean = true):void 158 | { 159 | while (_soundInstances.length > 0) 160 | { 161 | var si:SoundInstance = _soundInstances[0]; 162 | 163 | // Stop sound instance 164 | if (stopSoundBeforeDestroying) 165 | si.stop(); 166 | 167 | // Destroy sound instance 168 | si.destroy(); // This will automatically remove the instance from the list of sound instances 169 | } 170 | } 171 | 172 | //--------------------------------------------------------------------- 173 | //-- Tracks Management 174 | //--------------------------------------------------------------------- 175 | 176 | /** 177 | * Register a new track. 178 | * 179 | *

180 | * If `trimStartDuration` is negative the system will automatically look for it by using the auto trimming feature. 181 | * The `TrackConfiguration.trimDefaultSilenceThreshold` is then used for silence threshold. 182 | *

183 | * 184 | *

185 | * If `trimEndDuration` is negative the system will automatically look for it by using the auto trimming feature. 186 | * The `TrackConfiguration.trimDefaultSilenceThreshold` is then used for silence threshold. 187 | *

188 | */ 189 | public function registerTrack(type:String, track:Track, trimStartDuration:Number = 0, trimEndDuration:Number = 0, sampling:uint = 2048):TrackConfiguration 190 | { 191 | // Prevent to register twice an sound 192 | if (hasTrackRegistered(type)) 193 | throw new ArgumentError(LOG_PREFIX + " Sound type '" + type + "' is already registered."); 194 | 195 | // Add sound type 196 | var trackConfig:TrackConfiguration = new TrackConfiguration(track, type, trimStartDuration, trimEndDuration, sampling); 197 | _tracksByType[type] = trackConfig; 198 | 199 | // Emit event 200 | trackRegistered.dispatch(trackConfig); 201 | 202 | // Return the config 203 | return trackConfig; 204 | } 205 | 206 | /** 207 | * Unregister a track. 208 | */ 209 | public function unregisterTrack(type:String):void 210 | { 211 | // Prevent deleting unregistered track 212 | if (!hasTrackRegistered(type)) 213 | return; 214 | 215 | // Keep a reference of the config before deleting it 216 | var trackConfig:TrackConfiguration = _tracksByType[type]; 217 | 218 | // Delete entry 219 | delete _tracksByType[type]; 220 | 221 | // Destroy the configuration 222 | trackConfig.destroy(); 223 | 224 | // Emit event 225 | trackUnregistered.dispatch(trackConfig); 226 | } 227 | 228 | /** 229 | * Returns the list of all registered tracks. 230 | */ 231 | public function getRegisteredTracks():Vector. 232 | { 233 | var tracksConfig:Vector. = new Vector.(); 234 | for each (var trackConfig:TrackConfiguration in _tracksByType) 235 | tracksConfig.push(trackConfig); 236 | return tracksConfig; 237 | } 238 | 239 | /** 240 | * Check if a track is already registered. 241 | */ 242 | public function hasTrackRegistered(type:String):Boolean 243 | { 244 | return _tracksByType[type] != undefined; 245 | } 246 | 247 | /** 248 | * Find a registered track by type. 249 | * 250 | * @return the track or `null` if not found. 251 | */ 252 | public function findRegisteredTrack(type:String):TrackConfiguration 253 | { 254 | if (hasTrackRegistered(type)) 255 | return _tracksByType[type]; 256 | 257 | return null; 258 | } 259 | 260 | //--------------------------------------------------------------------- 261 | //-- Internal 262 | //--------------------------------------------------------------------- 263 | 264 | private function onSoundDestroyed(si:SoundInstance):void 265 | { 266 | // Look for the sound 267 | var index:int = _soundInstances.indexOf(si); 268 | if (index != -1) 269 | { 270 | // Remove from instances 271 | _soundInstances.removeAt(index); 272 | 273 | // Emit event 274 | soundInstanceRemoved.dispatch(si); 275 | } 276 | } 277 | 278 | private function onSoundCompleted(si:SoundInstance):void 279 | { 280 | // Remove sound instance 281 | if (si.manager == this) 282 | si.manager = null; // This will automatically trigger the unregistration 283 | else 284 | trace(LOG_PREFIX + " There is a Sound Manager inconsistency."); 285 | 286 | // Destroy or release the sound instance if requested 287 | if (si.freeWhenCompleted) 288 | { 289 | // Return to the pool if coming from it 290 | if (si.fromPool) 291 | releaseSoundInstanceToPool(si); 292 | else 293 | si.destroy(); 294 | } 295 | } 296 | 297 | /** 298 | * Create a sound instance of a given type. 299 | */ 300 | private function createSound(type:String):SoundInstance 301 | { 302 | // Unknown sound type 303 | if (_tracksByType[type] == undefined) 304 | throw new IllegalOperationError(LOG_PREFIX + " Sound type not registered: " + type); 305 | 306 | if (_isPoolingEnabled) 307 | return SoundInstancesPool.instance.acquireSound(_tracksByType[type], this); 308 | else 309 | return new SoundInstance(_tracksByType[type], this); // Sound instance will automatically register to the sound manager 310 | } 311 | 312 | /** 313 | * Remove a sound instance from the list of sound. 314 | */ 315 | internal function removeSoundInstance(si:SoundInstance, destroy:Boolean = false):void 316 | { 317 | // Look for the sound instance 318 | var index:int = _soundInstances.indexOf(si); 319 | if (index != -1) 320 | { 321 | // Stop listening to sound completion 322 | if (!si.isDestroyed) 323 | { 324 | si.destroyed.remove(onSoundDestroyed); 325 | si.completed.remove(onSoundCompleted); 326 | } 327 | 328 | // Remove from instances 329 | _soundInstances.removeAt(index); 330 | 331 | // Emit event 332 | soundInstanceRemoved.dispatch(si); 333 | 334 | // Destroy if asked and not destroyed yet 335 | if (!si.isDestroyed && destroy) 336 | si.destroy(); 337 | } 338 | else 339 | { 340 | trace(LOG_PREFIX + " Sound instance not found! Cannot remove."); 341 | } 342 | } 343 | 344 | /** 345 | * Add a sound instance to the list of sound. 346 | */ 347 | internal function addSoundInstance(si:SoundInstance):void 348 | { 349 | // Prevent adding an already added sound instance 350 | if (_soundInstances.indexOf(si) != -1) 351 | throw new ArgumentError(LOG_PREFIX + " Cannot add twice the same sound instance."); 352 | 353 | // Add the instance 354 | _soundInstances.push(si); 355 | 356 | // Listen to sound events 357 | si.completed.add(onSoundCompleted); 358 | si.destroyed.add(onSoundDestroyed); 359 | 360 | // Update (mixed) volume 361 | si.volume = si.volume; 362 | 363 | // Emit event 364 | soundInstanceAdded.dispatch(si); 365 | } 366 | 367 | //--------------------------------------------------------------------- 368 | //-- Pooling 369 | //--------------------------------------------------------------------- 370 | 371 | public function get isPoolingEnabled():Boolean 372 | { 373 | return _isPoolingEnabled; 374 | } 375 | 376 | public function set isPoolingEnabled(value:Boolean):void 377 | { 378 | _isPoolingEnabled = value; 379 | } 380 | 381 | /** 382 | * Release a sound instance to the pool. 383 | * 384 | * @param sound The sound instance to release 385 | */ 386 | public function releaseSoundInstanceToPool(sound:SoundInstance):void 387 | { 388 | if (!_isPoolingEnabled) 389 | throw new IllegalOperationError("Pooling is not enabled."); 390 | 391 | SoundInstancesPool.instance.releaseSound(sound); 392 | soundInstanceReleasedToPool.dispatch(sound, SoundInstancesPool.instance); 393 | } 394 | 395 | /** 396 | * Release all sound instances to the pool. 397 | */ 398 | public function releaseAllSoundInstancesToPool():void 399 | { 400 | if (!_isPoolingEnabled) 401 | throw new IllegalOperationError("Pooling is not enabled."); 402 | 403 | while (_soundInstances.length > 0) 404 | releaseSoundInstanceToPool(_soundInstances[0]); // This will automatically remove the instance from the list of sound instances 405 | } 406 | 407 | //--------------------------------------------------------------------- 408 | //-- Master Volume 409 | //--------------------------------------------------------------------- 410 | 411 | /** 412 | * Get the current master volume. 413 | */ 414 | public function get volume():Number 415 | { 416 | return _volume; 417 | } 418 | 419 | /** 420 | * Set the current master volume. 421 | * 422 | * This will update all contained sound instances. 423 | */ 424 | public function set volume(value:Number):void 425 | { 426 | _volume = value; 427 | 428 | // Update (mixed) volume of all the sound instances 429 | var si:SoundInstance; 430 | var length:int = _soundInstances.length; 431 | for (var i:int = 0; i < length; ++i) 432 | { 433 | si = _soundInstances[i]; 434 | si.volume = si.volume; // Force volume update from manager 435 | } 436 | } 437 | 438 | //--------------------------------------------------------------------- 439 | //-- Max Channel Capacity 440 | //--------------------------------------------------------------------- 441 | 442 | /** 443 | * Get the maximum possible number of sound instances playing simultaneously for this Sound Manager. 444 | */ 445 | public function get maxChannelCapacity():uint 446 | { 447 | return _maxChannelCapacity; 448 | } 449 | 450 | /** 451 | * Set the maximum possible number of sound instances playing simultaneously for this Sound Manager. 452 | * 453 | *

454 | * Note: the hard limit from Adobe AIR is MAX_CHANNELS (32) for all Sound Managers together. 455 | *

456 | */ 457 | public function set maxChannelCapacity(value:uint):void 458 | { 459 | if (value > MAX_CHANNELS) 460 | throw new ArgumentError(LOG_PREFIX + " Invalid argument. Max channel capacity cannot be bigger than " + MAX_CHANNELS + ". This is an hard limit from Adobe AIR."); 461 | 462 | _maxChannelCapacity = value; 463 | } 464 | } 465 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/Track.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.errors.IllegalOperationError; 14 | import flash.utils.ByteArray; 15 | 16 | /** 17 | * Base track class. 18 | * 19 | * This class wraps sound data & allows to retrieve it. 20 | * 21 | * @author Aurelien Da Campo 22 | */ 23 | public class Track 24 | { 25 | /** 26 | * The length of the current sound in milliseconds. 27 | */ 28 | public function get length():Number 29 | { 30 | throw new IllegalOperationError("[Track] Abstract method."); 31 | } 32 | 33 | /** 34 | * Extract samples. 35 | * 36 | * @param target A ByteArray object in which the extracted sound samples are placed. 37 | * @param length The number of sound samples to extract. A sample contains both the left and right channels — that is, two 32-bit floating-point values. 38 | * @param startPosition The sample at which extraction begins. If you don't specify a value, the first call to Sound.extract() starts at the beginning of the sound; subsequent calls without a value for startPosition progress sequentially through the file. 39 | * @return The number of samples written to the ByteArray specified in the target parameter. 40 | */ 41 | public function extract(target:ByteArray, length:Number, startPosition:Number = -1):Number 42 | { 43 | throw new IllegalOperationError("[Track] Abstract method."); 44 | } 45 | 46 | /** 47 | * Destroy the object and make it unusable. 48 | */ 49 | public function destroy():void 50 | { 51 | throw new IllegalOperationError("[Track] Abstract method."); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/TrackConfiguration.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.utils.ByteArray; 14 | 15 | /** 16 | * Track configuration 17 | * 18 | * @author Aurelien Da Campo 19 | */ 20 | public class TrackConfiguration 21 | { 22 | // Core 23 | private var _track:Track; 24 | private var _type:String; 25 | 26 | // Volume 27 | private var _baseVolume:Number = 1.0; 28 | 29 | // Trimming 30 | public static var trimDefaultSilenceThreshold:Number = 0.01; // Default silence threshold for automatic trimming. 31 | private var _trimStartDuration:Number = 0; // In milliseconds 32 | private var _trimEndDuration:Number = 0; // In milliseconds 33 | 34 | // Format 35 | private static const SAMPLE_SIZE:uint = 8; // 2*4 bytes 36 | private static const SAMPLE_RATE_MS:Number = 44.1; // Sampling rate per millisecond 37 | 38 | // Sampling options 39 | public static const SAMPLING_MIN_VALUE:uint = 2048; 40 | public static const SAMPLING_MAX_VALUE:uint = 8192; 41 | private var _sampling:uint = SAMPLING_MIN_VALUE; // Samples returned per sampling data request (min: 2048, max: 8192) 42 | 43 | /** 44 | * Track Configuration constructor. 45 | * 46 | *

47 | * If `trimStartDuration` is negative the system will automatically look for it by using the auto trimming feature. 48 | * The `TrackConfiguration.trimDefaultSilenceThreshold` is then used for silence threshold. 49 | *

50 | * 51 | *

52 | * If `trimEndDuration` is negative the system will automatically look for it by using the auto trimming feature. 53 | * The `TrackConfiguration.trimDefaultSilenceThreshold` is then used for silence threshold. 54 | *

55 | */ 56 | public function TrackConfiguration(track:Track, type:String, trimStartDuration:Number = 0, trimEndDuration:Number = 0, sampling:uint = SAMPLING_MIN_VALUE) 57 | { 58 | // Setup core variables 59 | _track = track; 60 | _type = type; 61 | 62 | // Update initial trimming durations 63 | this.trimStartDuration = trimStartDuration; 64 | this.trimEndDuration = trimEndDuration; 65 | 66 | // Update sampling 67 | this.sampling = sampling; 68 | } 69 | 70 | public function destroy():void 71 | { 72 | _track = null; 73 | _type = null; 74 | } 75 | 76 | public function clone():TrackConfiguration 77 | { 78 | var clone:TrackConfiguration = new TrackConfiguration(_track, _type, _trimStartDuration, _trimEndDuration); 79 | clone.sampling = _sampling; 80 | return clone; 81 | } 82 | 83 | public function get track():Track 84 | { 85 | return _track; 86 | } 87 | 88 | public function get type():String 89 | { 90 | return _type; 91 | } 92 | 93 | public function get baseVolume():Number 94 | { 95 | return _baseVolume; 96 | } 97 | 98 | public function set baseVolume(value:Number):void 99 | { 100 | _baseVolume = value; 101 | } 102 | 103 | public function get trimStartDuration():Number 104 | { 105 | return _trimStartDuration; 106 | } 107 | 108 | public function set trimStartDuration(value:Number):void 109 | { 110 | // Automatic trimming 111 | if (value < 0) 112 | value = findStartTrimDuration(trimDefaultSilenceThreshold); 113 | 114 | // Check for invalid trimming values 115 | if (value + _trimEndDuration > _track.length) 116 | throw new ArgumentError("[Track Configuration] Invalid trimming. Start + end trimming cannot be greater than the sound length. Sound length: " + _track.length + ", Trimming length: " + (value + _trimEndDuration)); 117 | 118 | // Update trimming 119 | _trimStartDuration = value; 120 | } 121 | 122 | public function get trimEndDuration():Number 123 | { 124 | return _trimEndDuration; 125 | } 126 | 127 | public function set trimEndDuration(value:Number):void 128 | { 129 | // Automatic trimming 130 | if (value < 0) 131 | value = findEndTrimDuration(trimDefaultSilenceThreshold, _track.length); 132 | 133 | // Check for invalid trimming values 134 | if (_trimStartDuration + value > _track.length) 135 | throw new ArgumentError("[Track Configuration] Invalid trimming duration. Start + end trimming durations cannot be greater than the sound length. Sound length: " + _track.length + ", Trimming length: " + (value + _trimEndDuration)); 136 | 137 | // Update trimming 138 | _trimEndDuration = value; 139 | } 140 | 141 | public function samplesToTime(samples:Number):Number 142 | { 143 | return Math.round(samples) / SAMPLE_RATE_MS; 144 | } 145 | 146 | public function timeToSamples(time:Number):Number 147 | { 148 | return Math.round(time * SAMPLE_RATE_MS); 149 | } 150 | 151 | public function findStartTrimDuration(silentThreshold:Number = 0.01, startPosition:Number = 0):Number 152 | { 153 | // End of the track reached 154 | if (startPosition > _track.length) 155 | return _track.length; 156 | 157 | // Extract bytes 158 | var readBytes:ByteArray = new ByteArray(); 159 | var samplesRead:Number = _track.extract(readBytes, _sampling, timeToSamples(startPosition)); 160 | readBytes.position = 0; 161 | 162 | // Find first non-silent sample from begin to end 163 | var nonSilentSampleFound:Boolean = false; 164 | var samplesCount:int = 0; 165 | const sampleSize:int = 8; // 2*4 bytes 166 | while (readBytes.bytesAvailable >= sampleSize) 167 | { 168 | // Get left & right float values 169 | var l0:Number = readBytes.readFloat(); 170 | var r0:Number = readBytes.readFloat(); 171 | 172 | // Detect non-silence 173 | if (Math.abs(l0) > silentThreshold || Math.abs(r0) > silentThreshold) 174 | return startPosition + samplesToTime(samplesCount); 175 | 176 | // Next sample 177 | ++samplesCount; 178 | } 179 | 180 | // Continue look-up... 181 | return findStartTrimDuration(silentThreshold, startPosition + samplesToTime(samplesRead)); 182 | } 183 | 184 | public function findEndTrimDuration(silentThreshold:Number = 0.01, startPosition:Number = 0):Number 185 | { 186 | // Extract bytes from the end 187 | var readBytes:ByteArray = new ByteArray(); 188 | var extractionPosition:Number = timeToSamples(startPosition) - _sampling; 189 | var sampleToRead:uint = _sampling; 190 | 191 | // Beginning of the track reached 192 | var beginReached:Boolean = false; 193 | if (extractionPosition < 0) 194 | { 195 | sampleToRead = _sampling - Math.abs(extractionPosition); 196 | extractionPosition = 0; 197 | beginReached = true; 198 | } 199 | 200 | // Read samples 201 | var samplesRead:uint = _track.extract(readBytes, sampleToRead, extractionPosition); 202 | 203 | // Find first non-silent sample from end to begin 204 | var nonSilentSampleFound:Boolean = false; 205 | var samplesCount:int = 0; 206 | var nextSamplePosition:int = samplesRead - SAMPLE_SIZE; 207 | while (nextSamplePosition >= 0) 208 | { 209 | // Move to the next sample position 210 | readBytes.position = nextSamplePosition; 211 | 212 | // Get left & right float values 213 | var l0:Number = readBytes.readFloat(); // 4 bytes 214 | var r0:Number = readBytes.readFloat(); // 4 bytes 215 | 216 | // Detect silence 217 | if (Math.abs(l0) > silentThreshold || Math.abs(r0) > silentThreshold) 218 | return _track.length - (startPosition - samplesToTime(samplesCount)); 219 | 220 | // Next sample 221 | ++samplesCount; 222 | nextSamplePosition = samplesRead - (samplesCount * SAMPLE_SIZE) - SAMPLE_SIZE; 223 | } 224 | 225 | // Continue look-up... 226 | if (!beginReached) 227 | return findEndTrimDuration(silentThreshold, startPosition - samplesToTime(samplesRead)); 228 | 229 | // Nothing found 230 | return _track.length; 231 | } 232 | 233 | public function findTrim(silentThreshold:Number = 0.01):void 234 | { 235 | // Reset trimming 236 | trimStartDuration = 0; 237 | trimEndDuration = 0; 238 | 239 | // Find trimming durations 240 | var startTrimDuration:Number = findStartTrimDuration(silentThreshold); 241 | var endTrimDuration:Number = findEndTrimDuration(silentThreshold, _track.length); 242 | 243 | // Check trimming duration validity 244 | if (startTrimDuration + endTrimDuration > _track.length) 245 | { 246 | trace("[Track Configuration] Impossible to find correct trimming durations. Sound is fully silent with silent threshold of " + silentThreshold + "."); 247 | return; 248 | } 249 | 250 | // Update trimming durations 251 | trimStartDuration = startTrimDuration; 252 | trimEndDuration = endTrimDuration; 253 | } 254 | 255 | public function get sampling():uint 256 | { 257 | return _sampling; 258 | } 259 | 260 | public function set sampling(value:uint):void 261 | { 262 | // Check for unsupported values 263 | if (value < SAMPLING_MIN_VALUE || value > SAMPLING_MAX_VALUE) 264 | { 265 | throw new ArgumentError("[Track Configuration] Invalid sampling value. Sampling value must be between " + SAMPLING_MIN_VALUE + " and " + SAMPLING_MAX_VALUE + " (included)."); 266 | } 267 | 268 | _sampling = value; 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /src/ch/adolio/sound/WavTrack.as: -------------------------------------------------------------------------------- 1 | // ================================================================================================= 2 | // 3 | // Syrinx - Sound Manager 4 | // Copyright (c) 2019-2022 Aurelien Da Campo (Adolio), All Rights Reserved. 5 | // 6 | // This program is free software. You can redistribute and/or modify it 7 | // in accordance with the terms of the accompanying license agreement. 8 | // 9 | // ================================================================================================= 10 | 11 | package ch.adolio.sound 12 | { 13 | import flash.errors.IllegalOperationError; 14 | import flash.utils.ByteArray; 15 | import flash.utils.Endian; 16 | import flash.utils.getTimer; 17 | 18 | /** 19 | * Wav sound data wrapper. 20 | * 21 | * Wave Format supported: 22 | * - PCM 16 bits, 1 channel, 44100 Hz 23 | * - PCM 16 bits, 2 channels, 44100 Hz 24 | * - IEEE Float 32 bits, 1 channel, 44100 Hz 25 | * - IEEE Float 32 bits, 2 channels, 44100 Hz 26 | * 27 | * @author Aurelien Da Campo 28 | */ 29 | public class WavTrack extends Track 30 | { 31 | // Descriptor 32 | private var _format:String; 33 | 34 | // Format 35 | private const WAVE_FORMAT_PCM:uint = 0x0001; 36 | private const WAVE_FORMAT_IEEE_FLOAT:uint = 0x0003; 37 | private var _fmtAudioFormat:uint; 38 | private var _fmtChannels:uint; 39 | private var _fmtSampleRate:int; 40 | private var _fmtByteRate:int; 41 | private var _fmtBlockAlign:int; 42 | private var _fmtBitsPerSample:int; 43 | 44 | // Data 45 | private var _dataSize:uint; // In bytes 46 | private var _dataSource:ByteArray; // Original data 47 | private var _dataStartPosition:uint; // Bytes position where data starts 48 | private var _length:Number; // In milliseconds 49 | private var _sampleGroupSize:uint; // Size of a sample in bytes (all channels included) 50 | private var _samplesGroupCount:uint; // Number of samples in data (all channels included) 51 | private var _predecoding:Boolean; // Allow to pre-convert bytes to IEEE float, two channels, 44'100 Hz for faster data extraction 52 | private var _decodedData:ByteArray; // In standard flash format: IEEE float, two channels, 44'100 Hz 53 | 54 | // Fact 55 | private var _samplePerChannel:int; 56 | 57 | // Extraction method 58 | private var _extractFunc:Function; 59 | 60 | // Debug 61 | private static const LOG_PREFIX:String = "[Wave Track]"; 62 | private var _verbose:Boolean = false; 63 | 64 | /** 65 | * Wav Track Constructor. 66 | * 67 | * @param data Wav raw data 68 | * @param predecode Convert data into native format (IEEE float, two channels, 44'100 Hz). 69 | * This improves preformance when extracting data during sampling. 70 | * Down sides: - Conversion may take a while... 71 | * - It takes more memory (the converted sound is stored in RAM) 72 | */ 73 | public function WavTrack(data:ByteArray, predecodeData:Boolean = false) 74 | { 75 | _predecoding = predecodeData; 76 | 77 | parseRawData(data); 78 | } 79 | 80 | //--------------------------------------------------------------------- 81 | //-- WAVE Format Parsing 82 | //--------------------------------------------------------------------- 83 | 84 | private function parseRawData(data:ByteArray):void 85 | { 86 | // Make sure data is treated as little endian 87 | data.endian = Endian.LITTLE_ENDIAN; 88 | 89 | // Browse chunks 90 | while (data.bytesAvailable) 91 | { 92 | // Read chunk id & size 93 | var chunkId:String = data.readUTFBytes(4); 94 | var chunkSize:uint = data.readInt(); 95 | 96 | // Debug 97 | if (_verbose) 98 | trace(chunkId + " (" + chunkSize + " bytes)"); 99 | 100 | // Parse chunk data 101 | switch (chunkId) 102 | { 103 | case "RIFF": parseRIFFChunk(data, chunkSize); break; 104 | case "fmt ": parseFmtSubChunk(data, chunkSize); break; 105 | case "data": parseDataSubChunk(data, chunkSize); break; 106 | default: 107 | // Ignored chunks 108 | data.position += chunkSize; // Jump to next chunk 109 | break; 110 | } 111 | } 112 | } 113 | 114 | private function parseRIFFChunk(data:ByteArray, size:uint):void 115 | { 116 | _format = data.readUTFBytes(4); 117 | 118 | if (_verbose) 119 | trace(" Format: " + _format); 120 | } 121 | 122 | private function parseFmtSubChunk(data:ByteArray, size:uint):void 123 | { 124 | // Fmt sub-chunk 125 | _fmtAudioFormat = data.readShort(); 126 | _fmtChannels = data.readShort(); 127 | _fmtSampleRate = data.readInt(); 128 | _fmtByteRate = data.readInt(); 129 | _fmtBlockAlign = data.readShort(); 130 | _fmtBitsPerSample = data.readShort(); 131 | 132 | // Format type validity check 133 | if (!(_fmtAudioFormat == WAVE_FORMAT_PCM || _fmtAudioFormat == WAVE_FORMAT_IEEE_FLOAT)) 134 | { 135 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: " + _format); 136 | } 137 | 138 | // PCM 16 bits format check 139 | if (_fmtAudioFormat == WAVE_FORMAT_PCM) 140 | { 141 | if (_fmtBitsPerSample != 16) 142 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: PCM Format must have 16 bits per sample."); 143 | } 144 | 145 | // IEEE Float 32 bits format check 146 | if (_fmtAudioFormat == WAVE_FORMAT_IEEE_FLOAT) 147 | { 148 | if (_fmtBitsPerSample != 32) 149 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: IEEE Format must have 32 bits per sample."); 150 | } 151 | 152 | // Sample rate check 153 | if (_fmtSampleRate != 44100) 154 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: Sample rate must be 44100 Hz."); 155 | 156 | // Min channel format check 157 | if (_fmtChannels < 1) 158 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: No channel found."); 159 | 160 | // Max channel format check 161 | if (_fmtChannels > 2) 162 | throw new IllegalOperationError(LOG_PREFIX + " Unsupported Wave Audio Format: Max 2 channels are supported."); 163 | 164 | // Debug 165 | if (_verbose) 166 | { 167 | trace(" Audio Format: " + _fmtAudioFormat + 168 | "\n Channels: " + _fmtChannels + 169 | "\n Sample Rate: " + _fmtSampleRate + 170 | "\n Byte Rate: " + _fmtByteRate + 171 | "\n Block Align: " + _fmtBlockAlign + 172 | "\n Bits Per Sample: " + _fmtBitsPerSample); 173 | } 174 | } 175 | 176 | private function parseDataSubChunk(data:ByteArray, size:uint):void 177 | { 178 | // Pre-Decoding 179 | if (_predecoding) 180 | { 181 | // Setup start time to record predecoding time 182 | var startTime:int = getTimer(); 183 | 184 | // Convert sound to IEEE 32bits two channels 185 | if (_fmtAudioFormat == WAVE_FORMAT_PCM) 186 | _fmtChannels == 1 ? decodePCM_Mono(data, size) : decodePCM_Stereo(data, size); 187 | else 188 | _fmtChannels == 1 ? decodeIEEE_Mono(data, size) : decodeIEEE_Stereo(data, size); 189 | 190 | // Print predecoding time 191 | if (_verbose) 192 | trace(LOG_PREFIX + " Pre-decoding time: " + (getTimer() - startTime) + " ms"); 193 | 194 | // Compute length 195 | _sampleGroupSize = (32 / 8) * 2; // IEEE 32bits two channels 196 | _samplesGroupCount = size / ((_fmtBitsPerSample / 8) * _fmtChannels); 197 | _length = (_samplesGroupCount / _fmtSampleRate) * 1000; 198 | 199 | // Setup extraction method 200 | _extractFunc = extractPreDecoded; 201 | } 202 | else 203 | { 204 | // Setup data info 205 | _dataSource = data; 206 | _dataStartPosition = data.position; 207 | _dataSize = size; 208 | 209 | // For further chunks 210 | data.position += size; 211 | 212 | // Compute length 213 | _sampleGroupSize = (_fmtBitsPerSample / 8) * _fmtChannels; 214 | _samplesGroupCount = size / _sampleGroupSize; 215 | _length = (_samplesGroupCount / _fmtSampleRate) * 1000; 216 | 217 | // Setup the sample extraction method 218 | if (_fmtAudioFormat == WAVE_FORMAT_PCM) 219 | _extractFunc = _fmtChannels == 1 ? extractPCM_Mono : extractPCM_Stereo; 220 | else 221 | _extractFunc = _fmtChannels == 1 ? extractIEEE_Mono : extractIEEE_Stereo; 222 | } 223 | } 224 | 225 | //--------------------------------------------------------------------- 226 | //-- Data Decoding 227 | //--------------------------------------------------------------------- 228 | 229 | private function decodePCM_Mono(data:ByteArray, size:uint):void 230 | { 231 | _decodedData = new ByteArray(); 232 | 233 | // Read all required samples 234 | var length:int = size / ((_fmtBitsPerSample / 8) * _fmtChannels); 235 | for (var i:int = 0; i < length; ++i) 236 | { 237 | var sample:Number = data.readShort() / 32767.0; // Normalize (MAX_SHORT - 1) 238 | _decodedData.writeFloat(sample); // Left 239 | _decodedData.writeFloat(sample); // Right 240 | } 241 | } 242 | 243 | private function decodePCM_Stereo(data:ByteArray, size:uint):void 244 | { 245 | _decodedData = new ByteArray(); 246 | 247 | // Read all required samples 248 | var length:int = size / ((_fmtBitsPerSample / 8) * _fmtChannels); 249 | for (var i:int = 0; i < length; ++i) 250 | { 251 | _decodedData.writeFloat(data.readShort() / 32767.0); // Left 252 | _decodedData.writeFloat(data.readShort() / 32767.0); // Right 253 | } 254 | } 255 | 256 | private function decodeIEEE_Mono(data:ByteArray, size:uint):void 257 | { 258 | _decodedData = new ByteArray(); 259 | 260 | // Read all required samples 261 | var length:int = size / ((_fmtBitsPerSample / 8) * _fmtChannels); 262 | for (var i:int = 0; i < length; ++i) 263 | { 264 | var sample:Number = data.readFloat(); 265 | _decodedData.writeFloat(sample); // Left 266 | _decodedData.writeFloat(sample); // Right 267 | } 268 | } 269 | 270 | private function decodeIEEE_Stereo(data:ByteArray, size:uint):void 271 | { 272 | _decodedData = new ByteArray(); 273 | 274 | // Read all required samples 275 | var length:int = size / ((_fmtBitsPerSample / 8) * _fmtChannels); 276 | for (var i:int = 0; i < length; ++i) 277 | { 278 | _decodedData.writeFloat(data.readFloat()); // Left 279 | _decodedData.writeFloat(data.readFloat()); // Right 280 | } 281 | 282 | // Bytes copy without read / write operations (doesn't work ;/ probably due to endianness) 283 | //_decodedData.writeBytes(data, data.position, size); 284 | } 285 | 286 | //--------------------------------------------------------------------- 287 | //-- Sound Interface 288 | //--------------------------------------------------------------------- 289 | 290 | public override function get length():Number 291 | { 292 | return _length; 293 | } 294 | 295 | public override function extract(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 296 | { 297 | return _extractFunc(target, samplesToRead, startPosition); 298 | } 299 | 300 | public function extractPreDecoded(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 301 | { 302 | // Read IEEE 32 bits, two channels data 303 | _decodedData.position = startPosition * 8; 304 | var availableSamples:int = _samplesGroupCount - startPosition; 305 | var samplesRead:int = Math.min(availableSamples, samplesToRead); 306 | target.writeBytes(_decodedData, _decodedData.position, samplesRead * 8); 307 | return samplesRead; 308 | } 309 | 310 | public override function destroy():void 311 | { 312 | // Clear data 313 | if (_decodedData) 314 | _decodedData.clear(); 315 | 316 | // Nullify references 317 | _dataSource = null; 318 | _decodedData = null; 319 | _extractFunc = null; 320 | } 321 | 322 | //--------------------------------------------------------------------- 323 | //-- PCM 16-bits extraction 324 | //--------------------------------------------------------------------- 325 | 326 | private function extractPCM_Mono(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 327 | { 328 | // Set reading position 329 | _dataSource.position = _dataStartPosition + startPosition * _sampleGroupSize; 330 | 331 | // Find required samples to read 332 | var availableSamples:int = _samplesGroupCount - startPosition; 333 | var samplesRead:int = Math.min(availableSamples, samplesToRead); 334 | 335 | // Read all required samples 336 | for (var i:int = 0; i < samplesRead; ++i) 337 | { 338 | var sample:Number = _dataSource.readShort() / 32767.0; // Normalize (MAX_SHORT - 1) 339 | target.writeFloat(sample); // Left 340 | target.writeFloat(sample); // Right 341 | } 342 | 343 | // Returns the read sample 344 | return samplesRead; 345 | } 346 | 347 | private function extractPCM_Stereo(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 348 | { 349 | // Set reading position 350 | _dataSource.position = _dataStartPosition + startPosition * _sampleGroupSize; 351 | 352 | // Find required samples to read 353 | var availableSamples:int = _samplesGroupCount - startPosition; 354 | var samplesRead:int = Math.min(availableSamples, samplesToRead); 355 | 356 | // Read all required samples 357 | for (var i:int = 0; i < samplesRead; ++i) 358 | { 359 | target.writeFloat(_dataSource.readShort() / 32767.0); // Left 360 | target.writeFloat(_dataSource.readShort() / 32767.0); // Right 361 | } 362 | 363 | // Returns the read sample 364 | return samplesRead; 365 | } 366 | 367 | //--------------------------------------------------------------------- 368 | //-- IEEE Float 32-bits extraction 369 | //--------------------------------------------------------------------- 370 | 371 | private function extractIEEE_Mono(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 372 | { 373 | // Set reading position 374 | _dataSource.position = _dataStartPosition + startPosition * _sampleGroupSize; 375 | 376 | // Find required samples to read 377 | var availableSamples:int = _samplesGroupCount - startPosition; 378 | var samplesRead:int = Math.min(availableSamples, samplesToRead); 379 | 380 | // Read all required samples 381 | for (var i:int = 0; i < samplesRead; ++i) 382 | { 383 | var sample:Number = _dataSource.readFloat(); 384 | target.writeFloat(sample); // Left 385 | target.writeFloat(sample); // Right 386 | } 387 | 388 | // Returns the read sample 389 | return samplesRead; 390 | } 391 | 392 | private function extractIEEE_Stereo(target:ByteArray, samplesToRead:Number, startPosition:Number = -1):Number 393 | { 394 | // Set reading position 395 | _dataSource.position = _dataStartPosition + startPosition * _sampleGroupSize; 396 | 397 | // Find required samples to read 398 | var availableSamples:int = _samplesGroupCount - startPosition; 399 | var samplesRead:int = Math.min(availableSamples, samplesToRead); 400 | 401 | // Read all required samples 402 | for (var i:int = 0; i < samplesRead; ++i) 403 | { 404 | target.writeFloat(_dataSource.readFloat()); // Left 405 | target.writeFloat(_dataSource.readFloat()); // Right 406 | } 407 | 408 | // Returns the read sample 409 | return samplesRead; 410 | } 411 | } 412 | } --------------------------------------------------------------------------------