├── .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 | 
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 | }
--------------------------------------------------------------------------------