@quenty/soundplayer 1.0.1-canary.402.a911cda.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE.md +21 -0
- package/README.md +23 -0
- package/default.project.json +6 -0
- package/package.json +52 -0
- package/src/Client/Loops/Layered/LayeredLoopedSoundPlayer.lua +169 -0
- package/src/Client/Loops/Layered/LayeredLoopedSoundPlayer.story.lua +160 -0
- package/src/Client/Loops/LoopedSoundPlayer.lua +449 -0
- package/src/Client/Loops/LoopedSoundPlayer.story.lua +125 -0
- package/src/Client/Loops/SimpleLoopedSoundPlayer.lua +63 -0
- package/src/Client/Loops/SimpleLoopedSoundPlayer.story.lua +68 -0
- package/src/Client/Schedule/SoundLoopScheduleUtils.lua +64 -0
- package/src/node_modules.project.json +7 -0
- package/test/default.project.json +11 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## 1.0.1-canary.402.a911cda.0 (2023-08-23)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Remove unused code ([cb0e625](https://github.com/Quenty/NevermoreEngine/commit/cb0e62593e010fb6df9779f885e9abf9754f871c))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Add sound layer playback system ([20ffe12](https://github.com/Quenty/NevermoreEngine/commit/20ffe12c1029f618ed689961ab5990c7da6855f7))
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2014-2023 James Onnen (Quenty)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## SoundPlayer
|
|
2
|
+
|
|
3
|
+
<div align="center">
|
|
4
|
+
<a href="http://quenty.github.io/NevermoreEngine/">
|
|
5
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/docs.yml/badge.svg" alt="Documentation status" />
|
|
6
|
+
</a>
|
|
7
|
+
<a href="https://discord.gg/mhtGUS8">
|
|
8
|
+
<img src="https://img.shields.io/discord/385151591524597761?color=5865F2&label=discord&logo=discord&logoColor=white" alt="Discord" />
|
|
9
|
+
</a>
|
|
10
|
+
<a href="https://github.com/Quenty/NevermoreEngine/actions">
|
|
11
|
+
<img src="https://github.com/Quenty/NevermoreEngine/actions/workflows/build.yml/badge.svg" alt="Build and release status" />
|
|
12
|
+
</a>
|
|
13
|
+
</div>
|
|
14
|
+
|
|
15
|
+
Sound playback helper
|
|
16
|
+
|
|
17
|
+
<div align="center"><a href="https://quenty.github.io/NevermoreEngine/api/LoopedSoundPlayer">View docs →</a></div>
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
npm install @quenty/soundplayer --save
|
|
23
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quenty/soundplayer",
|
|
3
|
+
"version": "1.0.1-canary.402.a911cda.0",
|
|
4
|
+
"description": "Sound playback helper",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"Roblox",
|
|
7
|
+
"Nevermore",
|
|
8
|
+
"Lua",
|
|
9
|
+
"soundplayer"
|
|
10
|
+
],
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Quenty/NevermoreEngine/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/Quenty/NevermoreEngine.git",
|
|
17
|
+
"directory": "src/soundplayer/"
|
|
18
|
+
},
|
|
19
|
+
"funding": {
|
|
20
|
+
"type": "patreon",
|
|
21
|
+
"url": "https://www.patreon.com/quenty"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"contributors": [
|
|
25
|
+
"Quenty"
|
|
26
|
+
],
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@quenty/adorneeutils": "3.1.0",
|
|
29
|
+
"@quenty/baseobject": "6.2.2-canary.402.a911cda.0",
|
|
30
|
+
"@quenty/blend": "6.26.1-canary.402.a911cda.0",
|
|
31
|
+
"@quenty/brio": "8.16.1-canary.402.a911cda.0",
|
|
32
|
+
"@quenty/instanceutils": "7.18.1-canary.402.a911cda.0",
|
|
33
|
+
"@quenty/loader": "6.2.2-canary.402.a911cda.0",
|
|
34
|
+
"@quenty/maid": "2.5.1-canary.402.a911cda.0",
|
|
35
|
+
"@quenty/numberrangeutils": "3.0.0",
|
|
36
|
+
"@quenty/promise": "6.7.1-canary.402.a911cda.0",
|
|
37
|
+
"@quenty/promisemaid": "1.2.1-canary.402.a911cda.0",
|
|
38
|
+
"@quenty/randomutils": "2.2.1-canary.402.a911cda.0",
|
|
39
|
+
"@quenty/rbxasset": "1.1.1-canary.402.a911cda.0",
|
|
40
|
+
"@quenty/rx": "7.14.1-canary.402.a911cda.0",
|
|
41
|
+
"@quenty/signal": "2.4.0",
|
|
42
|
+
"@quenty/sounds": "6.9.1-canary.402.a911cda.0",
|
|
43
|
+
"@quenty/table": "3.2.1-canary.402.a911cda.0",
|
|
44
|
+
"@quenty/transitionmodel": "1.17.1-canary.402.a911cda.0",
|
|
45
|
+
"@quenty/valueobject": "7.21.1-canary.402.a911cda.0",
|
|
46
|
+
"@quentystudios/t": "^3.0.0"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"gitHead": "a911cdaf4f1039b599528cec17b027f4660e4fd8"
|
|
52
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class LayeredLoopedSoundPlayer
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local SpringTransitionModel = require("SpringTransitionModel")
|
|
8
|
+
local ValueObject = require("ValueObject")
|
|
9
|
+
local LoopedSoundPlayer = require("LoopedSoundPlayer")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Rx = require("Rx")
|
|
12
|
+
local SoundUtils = require("SoundUtils")
|
|
13
|
+
local SoundLoopScheduleUtils = require("SoundLoopScheduleUtils")
|
|
14
|
+
|
|
15
|
+
local LayeredLoopedSoundPlayer = setmetatable({}, SpringTransitionModel)
|
|
16
|
+
LayeredLoopedSoundPlayer.ClassName = "LayeredLoopedSoundPlayer"
|
|
17
|
+
LayeredLoopedSoundPlayer.__index = LayeredLoopedSoundPlayer
|
|
18
|
+
|
|
19
|
+
function LayeredLoopedSoundPlayer.new(soundParent)
|
|
20
|
+
local self = setmetatable(SpringTransitionModel.new(), LayeredLoopedSoundPlayer)
|
|
21
|
+
|
|
22
|
+
self._soundParent = ValueObject.new(nil)
|
|
23
|
+
self._maid:GiveTask(self._soundParent)
|
|
24
|
+
|
|
25
|
+
self._bpm = ValueObject.new(nil)
|
|
26
|
+
self._maid:GiveTask(self._bpm)
|
|
27
|
+
|
|
28
|
+
self._defaultCrossFadeTime = ValueObject.new(0.5, "number")
|
|
29
|
+
self._maid:GiveTask(self._defaultCrossFadeTime)
|
|
30
|
+
|
|
31
|
+
self._layerMaid = Maid.new()
|
|
32
|
+
self._maid:GiveTask(self._layerMaid)
|
|
33
|
+
|
|
34
|
+
self._volumeMultiplier = ValueObject.new(1, "number")
|
|
35
|
+
self._maid:GiveTask(self._volumeMultiplier)
|
|
36
|
+
|
|
37
|
+
self._layers = {}
|
|
38
|
+
|
|
39
|
+
if soundParent then
|
|
40
|
+
self:SetSoundParent(soundParent)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
return self
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
function LayeredLoopedSoundPlayer:SetDefaultCrossFadeTime(crossFadeTime)
|
|
47
|
+
return self._defaultCrossFadeTime:Mount(crossFadeTime)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
function LayeredLoopedSoundPlayer:SetVolumeMultiplier(volumeMultiplier)
|
|
51
|
+
self._volumeMultiplier.Value = volumeMultiplier
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
function LayeredLoopedSoundPlayer:SetBPM(bpm)
|
|
55
|
+
assert(type(bpm) == "number" or bpm == nil, "Bad bpm")
|
|
56
|
+
|
|
57
|
+
self._bpm.Value = bpm
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
function LayeredLoopedSoundPlayer:SetSoundParent(soundParent)
|
|
61
|
+
self._soundParent.Value = soundParent
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function LayeredLoopedSoundPlayer:Swap(layerId, soundId, scheduleOptions)
|
|
65
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
66
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
67
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
68
|
+
|
|
69
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
70
|
+
layer:Swap(soundId, scheduleOptions)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
function LayeredLoopedSoundPlayer:SwapOnLoop(layerId, soundId, scheduleOptions)
|
|
74
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
75
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
76
|
+
|
|
77
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
78
|
+
layer:SwapOnLoop(soundId, scheduleOptions)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function LayeredLoopedSoundPlayer:SwapToSamples(layerId, soundId, scheduleOptions)
|
|
82
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
83
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
84
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
85
|
+
|
|
86
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
87
|
+
layer:SwapToSamples(soundId, scheduleOptions)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
function LayeredLoopedSoundPlayer:SwapToChoice(layerId, soundIdList, scheduleOptions)
|
|
91
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
92
|
+
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
93
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
94
|
+
|
|
95
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
96
|
+
layer:SwapToChoice(soundIdList, scheduleOptions)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
function LayeredLoopedSoundPlayer:PlayOnce(layerId, soundIdList, scheduleOptions)
|
|
100
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
101
|
+
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
102
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
103
|
+
|
|
104
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
105
|
+
layer:PlayOnce(soundIdList, scheduleOptions)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
function LayeredLoopedSoundPlayer:PlayOnceOnLoop(layerId, soundId, scheduleOptions)
|
|
109
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
110
|
+
|
|
111
|
+
local layer = self:_getOrCreateLayer(layerId)
|
|
112
|
+
layer:PlayOnceOnLoop(soundId, scheduleOptions)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
function LayeredLoopedSoundPlayer:_getOrCreateLayer(layerId)
|
|
116
|
+
if self._layers[layerId] then
|
|
117
|
+
return self._layers[layerId]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
local maid = Maid.new()
|
|
121
|
+
|
|
122
|
+
local layer = LoopedSoundPlayer.new()
|
|
123
|
+
layer:SetDoSyncSoundPlayback(true)
|
|
124
|
+
maid:GiveTask(layer)
|
|
125
|
+
|
|
126
|
+
maid:GiveTask(layer:SetCrossFadeTime(self._defaultCrossFadeTime:Observe()))
|
|
127
|
+
|
|
128
|
+
maid:GiveTask(self._bpm:Observe():Subscribe(function(bpm)
|
|
129
|
+
layer:SetBPM(bpm)
|
|
130
|
+
end))
|
|
131
|
+
|
|
132
|
+
maid:GiveTask(self:ObserveVisible():Subscribe(function(isVisible, doNotAnimate)
|
|
133
|
+
layer:SetVisible(isVisible, doNotAnimate)
|
|
134
|
+
end))
|
|
135
|
+
|
|
136
|
+
maid:GiveTask(self._soundParent:Observe():Subscribe(function(parent)
|
|
137
|
+
layer:SetSoundParent(parent)
|
|
138
|
+
end))
|
|
139
|
+
|
|
140
|
+
maid:GiveTask(Rx.combineLatest({
|
|
141
|
+
visible = self:ObserveRenderStepped();
|
|
142
|
+
multiplier = self._volumeMultiplier:Observe();
|
|
143
|
+
}):Subscribe(function(state)
|
|
144
|
+
layer:SetVolumeMultiplier(state.multiplier*state.visible)
|
|
145
|
+
end))
|
|
146
|
+
|
|
147
|
+
self._layers[layerId] = layer
|
|
148
|
+
maid:GiveTask(function()
|
|
149
|
+
if self._layers[layerId] == layer then
|
|
150
|
+
self._layers[layerId] = nil
|
|
151
|
+
end
|
|
152
|
+
end)
|
|
153
|
+
|
|
154
|
+
self._layerMaid[layerId] = maid
|
|
155
|
+
|
|
156
|
+
return layer
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
function LayeredLoopedSoundPlayer:StopLayer(layerId)
|
|
160
|
+
assert(type(layerId) == "string", 'Bad layerId')
|
|
161
|
+
|
|
162
|
+
self._layerMaid[layerId] = nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
function LayeredLoopedSoundPlayer:StopAll()
|
|
166
|
+
self._layerMaid:DoCleaning()
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
return LayeredLoopedSoundPlayer
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class LayeredLoopedSoundPlayer.story
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local Maid = require("Maid")
|
|
8
|
+
local LayeredLoopedSoundPlayer = require("LayeredLoopedSoundPlayer")
|
|
9
|
+
local Blend = require("Blend")
|
|
10
|
+
|
|
11
|
+
return function(target)
|
|
12
|
+
local maid = Maid.new()
|
|
13
|
+
|
|
14
|
+
local layeredLoopedSoundPlayer = LayeredLoopedSoundPlayer.new()
|
|
15
|
+
layeredLoopedSoundPlayer:SetSoundParent(target)
|
|
16
|
+
layeredLoopedSoundPlayer:SetBPM(95)
|
|
17
|
+
maid:GiveTask(layeredLoopedSoundPlayer)
|
|
18
|
+
|
|
19
|
+
local function initial()
|
|
20
|
+
layeredLoopedSoundPlayer:SwapToChoice("drums", {
|
|
21
|
+
{
|
|
22
|
+
SoundId = "rbxassetid://14478151709";
|
|
23
|
+
Volume = 0.1;
|
|
24
|
+
};
|
|
25
|
+
{
|
|
26
|
+
SoundId = "rbxassetid://14478738244";
|
|
27
|
+
Volume = 0.1;
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
layeredLoopedSoundPlayer:SwapToChoice("rifts", {
|
|
31
|
+
{
|
|
32
|
+
SoundId = "rbxassetid://14478152812";
|
|
33
|
+
Volume = 0.2;
|
|
34
|
+
};
|
|
35
|
+
{
|
|
36
|
+
SoundId = "rbxassetid://14478729478";
|
|
37
|
+
Volume = 0.015;
|
|
38
|
+
};
|
|
39
|
+
})
|
|
40
|
+
end
|
|
41
|
+
initial()
|
|
42
|
+
|
|
43
|
+
layeredLoopedSoundPlayer:Show()
|
|
44
|
+
|
|
45
|
+
local function button(props)
|
|
46
|
+
return Blend.New "TextButton" {
|
|
47
|
+
Text = props.Text;
|
|
48
|
+
AutoButtonColor = true;
|
|
49
|
+
Font = Enum.Font.FredokaOne;
|
|
50
|
+
Size = UDim2.new(0, 100, 0, 30);
|
|
51
|
+
|
|
52
|
+
Blend.New "UICorner" {
|
|
53
|
+
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
[Blend.OnEvent "Activated"] = function()
|
|
57
|
+
props.OnActivated();
|
|
58
|
+
end;
|
|
59
|
+
};
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
maid:GiveTask(Blend.mount(target, {
|
|
63
|
+
Blend.New "Frame" {
|
|
64
|
+
Name = "ButtonContainer";
|
|
65
|
+
BackgroundTransparency = 1;
|
|
66
|
+
Position = UDim2.new(0.5, 0, 0, 5);
|
|
67
|
+
AnchorPoint = Vector2.new(0.5, 0);
|
|
68
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
69
|
+
|
|
70
|
+
Blend.New "UIListLayout" {
|
|
71
|
+
FillDirection = Enum.FillDirection.Horizontal;
|
|
72
|
+
Padding = UDim.new(0, 5);
|
|
73
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
button({
|
|
77
|
+
Text = "Toggle";
|
|
78
|
+
OnActivated = function()
|
|
79
|
+
layeredLoopedSoundPlayer:Toggle()
|
|
80
|
+
end;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
button({
|
|
84
|
+
Text = "Reset";
|
|
85
|
+
OnActivated = function()
|
|
86
|
+
initial()
|
|
87
|
+
end;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
button({
|
|
91
|
+
Text = "Combat equip";
|
|
92
|
+
OnActivated = function()
|
|
93
|
+
layeredLoopedSoundPlayer:SwapToChoice("drums", {
|
|
94
|
+
"rbxassetid://14478154829";
|
|
95
|
+
"rbxassetid://14478714545";
|
|
96
|
+
"rbxassetid://14478772830";
|
|
97
|
+
"rbxassetid://14478897865";
|
|
98
|
+
})
|
|
99
|
+
layeredLoopedSoundPlayer:PlayOnceOnLoop("rifts", nil)
|
|
100
|
+
end;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
button({
|
|
104
|
+
Text = "On target lock";
|
|
105
|
+
OnActivated = function()
|
|
106
|
+
layeredLoopedSoundPlayer:SwapToChoice("drums", {
|
|
107
|
+
{
|
|
108
|
+
SoundId = "rbxassetid://14478150956";
|
|
109
|
+
Volume = 0.1;
|
|
110
|
+
};
|
|
111
|
+
{
|
|
112
|
+
SoundId = "rbxassetid://14478721669";
|
|
113
|
+
Volume = 0.2;
|
|
114
|
+
};
|
|
115
|
+
"rbxassetid://14478154829";
|
|
116
|
+
"rbxassetid://14478764914";
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
layeredLoopedSoundPlayer:SwapToChoice("rifts", {
|
|
120
|
+
"rbxassetid://14478145963";
|
|
121
|
+
"rbxassetid://14478156714";
|
|
122
|
+
{
|
|
123
|
+
SoundId = "rbxassetid://14478777472";
|
|
124
|
+
Volume = 0.1;
|
|
125
|
+
};
|
|
126
|
+
{
|
|
127
|
+
SoundId = "rbxassetid://14478793045";
|
|
128
|
+
Volume = 0.1;
|
|
129
|
+
};
|
|
130
|
+
})
|
|
131
|
+
end;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
button({
|
|
135
|
+
Text = "On low health";
|
|
136
|
+
OnActivated = function()
|
|
137
|
+
layeredLoopedSoundPlayer:SwapToChoice("drums", {
|
|
138
|
+
"rbxassetid://14478746326";
|
|
139
|
+
"rbxassetid://14478767498";
|
|
140
|
+
"rbxassetid://14478797936"; -- record scratch
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
end;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
button({
|
|
148
|
+
Text = "Target drop";
|
|
149
|
+
OnActivated = function()
|
|
150
|
+
layeredLoopedSoundPlayer:PlayOnceOnLoop("rifts", "rbxassetid://14478158396")
|
|
151
|
+
end;
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
return function()
|
|
158
|
+
maid:DoCleaning()
|
|
159
|
+
end
|
|
160
|
+
end
|
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class LoopedSoundPlayer
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local RunService = game:GetService("RunService")
|
|
8
|
+
|
|
9
|
+
local Maid = require("Maid")
|
|
10
|
+
local Promise = require("Promise")
|
|
11
|
+
local PromiseMaidUtils = require("PromiseMaidUtils")
|
|
12
|
+
local RandomSampler = require("RandomSampler")
|
|
13
|
+
local RandomUtils = require("RandomUtils")
|
|
14
|
+
local Rx = require("Rx")
|
|
15
|
+
local RxInstanceUtils = require("RxInstanceUtils")
|
|
16
|
+
local Signal = require("Signal")
|
|
17
|
+
local SimpleLoopedSoundPlayer = require("SimpleLoopedSoundPlayer")
|
|
18
|
+
local SoundLoopScheduleUtils = require("SoundLoopScheduleUtils")
|
|
19
|
+
local SoundPromiseUtils = require("SoundPromiseUtils")
|
|
20
|
+
local SoundUtils = require("SoundUtils")
|
|
21
|
+
local SpringTransitionModel = require("SpringTransitionModel")
|
|
22
|
+
local ValueObject = require("ValueObject")
|
|
23
|
+
|
|
24
|
+
local LoopedSoundPlayer = setmetatable({}, SpringTransitionModel)
|
|
25
|
+
LoopedSoundPlayer.ClassName = "LoopedSoundPlayer"
|
|
26
|
+
LoopedSoundPlayer.__index = LoopedSoundPlayer
|
|
27
|
+
|
|
28
|
+
function LoopedSoundPlayer.new(soundId, soundParent)
|
|
29
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
30
|
+
|
|
31
|
+
local self = setmetatable(SpringTransitionModel.new(), LoopedSoundPlayer)
|
|
32
|
+
|
|
33
|
+
self._currentSoundLooped = Signal.new()
|
|
34
|
+
self._maid:GiveTask(self._currentSoundLooped)
|
|
35
|
+
|
|
36
|
+
self._currentSoundLoopedAfterDelay = Signal.new()
|
|
37
|
+
self._maid:GiveTask(self._currentSoundLoopedAfterDelay)
|
|
38
|
+
|
|
39
|
+
self:SetSpeed(10)
|
|
40
|
+
|
|
41
|
+
self._bpm = ValueObject.new(nil)
|
|
42
|
+
self._maid:GiveTask(self._bpm)
|
|
43
|
+
|
|
44
|
+
self._soundParent = ValueObject.new(nil)
|
|
45
|
+
self._maid:GiveTask(self._soundParent)
|
|
46
|
+
|
|
47
|
+
self._crossFadeTime = ValueObject.new(0.5, "number")
|
|
48
|
+
self._maid:GiveTask(self._crossFadeTime)
|
|
49
|
+
|
|
50
|
+
self._volumeMultiplier = ValueObject.new(1, "number")
|
|
51
|
+
self._maid:GiveTask(self._volumeMultiplier)
|
|
52
|
+
|
|
53
|
+
self._doSyncSoundPlayback = ValueObject.new(false, "boolean")
|
|
54
|
+
self._maid:GiveTask(self._doSyncSoundPlayback)
|
|
55
|
+
|
|
56
|
+
self._currentActiveSound = ValueObject.new(nil)
|
|
57
|
+
self._maid:GiveTask(self._currentActiveSound)
|
|
58
|
+
|
|
59
|
+
self._currentSoundId = ValueObject.new(soundId)
|
|
60
|
+
self._maid:GiveTask(self._currentSoundId)
|
|
61
|
+
|
|
62
|
+
self._defaultScheduleOptions = SoundLoopScheduleUtils.default()
|
|
63
|
+
|
|
64
|
+
self._currentLoopSchedule = ValueObject.new(self._defaultScheduleOptions)
|
|
65
|
+
self._maid:GiveTask(self._currentLoopSchedule)
|
|
66
|
+
|
|
67
|
+
if soundParent then
|
|
68
|
+
self:SetSoundParent(soundParent)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if soundId then
|
|
72
|
+
self:Swap(soundId)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
self:_setupRender()
|
|
76
|
+
|
|
77
|
+
return self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
function LoopedSoundPlayer:SetCrossFadeTime(crossFadeTime)
|
|
81
|
+
return self._crossFadeTime:Mount(crossFadeTime)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
function LoopedSoundPlayer:SetVolumeMultiplier(volume)
|
|
85
|
+
self._volumeMultiplier.Value = volume
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
function LoopedSoundPlayer:SetBPM(bpm)
|
|
89
|
+
assert(type(bpm) == "number" or bpm == nil, "Bad bpm")
|
|
90
|
+
|
|
91
|
+
self._bpm.Value = bpm
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
function LoopedSoundPlayer:SetSoundParent(parent)
|
|
95
|
+
self._soundParent.Value = parent
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
function LoopedSoundPlayer:Swap(soundId, loopSchedule)
|
|
99
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
100
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
101
|
+
|
|
102
|
+
local maid = Maid.new()
|
|
103
|
+
|
|
104
|
+
maid:GiveTask(self:_scheduleFirstPlay(loopSchedule, function()
|
|
105
|
+
self._currentLoopSchedule.Value = loopSchedule
|
|
106
|
+
self._currentSoundId.Value = soundId
|
|
107
|
+
end))
|
|
108
|
+
|
|
109
|
+
self._maid._swappingTo = maid
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
function LoopedSoundPlayer:SetDoSyncSoundPlayback(doSyncSoundPlayback)
|
|
113
|
+
self._doSyncSoundPlayback.Value = doSyncSoundPlayback
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
function LoopedSoundPlayer:_setupRender()
|
|
117
|
+
self._maid:GiveTask(self._currentSoundId:ObserveBrio(function(value)
|
|
118
|
+
return value ~= nil
|
|
119
|
+
end):Subscribe(function(brio)
|
|
120
|
+
if brio:IsDead() then
|
|
121
|
+
return
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
local maid = brio:ToMaid()
|
|
126
|
+
local soundId = brio:GetValue()
|
|
127
|
+
|
|
128
|
+
maid:GiveTask(self:_renderSoundPlayer(soundId))
|
|
129
|
+
end))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
function LoopedSoundPlayer:_renderSoundPlayer(soundId)
|
|
133
|
+
local maid = Maid.new()
|
|
134
|
+
|
|
135
|
+
local renderMaid = Maid.new()
|
|
136
|
+
local soundPlayer = SimpleLoopedSoundPlayer.new(soundId)
|
|
137
|
+
soundPlayer:SetTransitionTime(self._crossFadeTime)
|
|
138
|
+
renderMaid:GiveTask(soundPlayer)
|
|
139
|
+
|
|
140
|
+
renderMaid:GiveTask(Rx.combineLatest({
|
|
141
|
+
bpm = self._bpm:Observe();
|
|
142
|
+
isLoaded = Rx.fromPromise(SoundPromiseUtils.promiseLoaded(soundPlayer.Sound));
|
|
143
|
+
doSyncSoundPlayback = self._doSyncSoundPlayback:Observe();
|
|
144
|
+
timeLength = RxInstanceUtils.observeProperty(soundPlayer.Sound, "TimeLength");
|
|
145
|
+
}):Subscribe(function(state)
|
|
146
|
+
local syncMaid = Maid.new()
|
|
147
|
+
|
|
148
|
+
if state.doSyncSoundPlayback then
|
|
149
|
+
if state.bpm then
|
|
150
|
+
local bps = state.bpm/60
|
|
151
|
+
local beatTime = 1/bps
|
|
152
|
+
local truncatedTimeLength = math.floor(state.timeLength/beatTime) * beatTime
|
|
153
|
+
local currentTimePosition = soundPlayer.Sound.TimePosition
|
|
154
|
+
local clockDistanceIntoBeat = os.clock() % beatTime
|
|
155
|
+
local soundDistanceIntoBeat = currentTimePosition % beatTime
|
|
156
|
+
|
|
157
|
+
-- Skip to next beat
|
|
158
|
+
local offset = (beatTime + (clockDistanceIntoBeat - soundDistanceIntoBeat)) % beatTime
|
|
159
|
+
soundPlayer.Sound.TimePosition = currentTimePosition + offset
|
|
160
|
+
|
|
161
|
+
syncMaid:GiveTask(RunService.RenderStepped:Connect(function()
|
|
162
|
+
if soundPlayer.Sound.TimePosition > truncatedTimeLength then
|
|
163
|
+
soundPlayer.Sound.TimePosition = soundPlayer.Sound.TimePosition % truncatedTimeLength
|
|
164
|
+
|
|
165
|
+
if self.Destroy then
|
|
166
|
+
if self._currentActiveSound.Value == soundPlayer.Sound then
|
|
167
|
+
self._currentSoundLooped:Fire()
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end))
|
|
172
|
+
else
|
|
173
|
+
soundPlayer.Sound.TimePosition = os.clock() % state.timeLength
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
renderMaid._syncing = syncMaid
|
|
178
|
+
end))
|
|
179
|
+
|
|
180
|
+
maid:GiveTask(Rx.combineLatest({
|
|
181
|
+
loopSchedule = self._currentLoopSchedule:Observe();
|
|
182
|
+
}):Pipe({
|
|
183
|
+
Rx.throttleDefer();
|
|
184
|
+
}):Subscribe(function(state)
|
|
185
|
+
local scheduleMaid = Maid.new()
|
|
186
|
+
|
|
187
|
+
scheduleMaid:GiveTask(self:_setupLoopScheduling(soundPlayer, state.loopSchedule))
|
|
188
|
+
|
|
189
|
+
renderMaid._loopMaid = scheduleMaid
|
|
190
|
+
end))
|
|
191
|
+
|
|
192
|
+
maid:GiveTask(soundPlayer.Sound.DidLoop:Connect(function()
|
|
193
|
+
self._currentSoundLooped:Fire()
|
|
194
|
+
end))
|
|
195
|
+
|
|
196
|
+
self._currentActiveSound.Value = soundPlayer.Sound
|
|
197
|
+
|
|
198
|
+
maid:GiveTask(function()
|
|
199
|
+
if self._currentActiveSound.Value == soundPlayer.Sound then
|
|
200
|
+
self._currentActiveSound.Value = nil
|
|
201
|
+
end
|
|
202
|
+
end)
|
|
203
|
+
|
|
204
|
+
renderMaid:GiveTask(self._soundParent:Observe():Subscribe(function(parent)
|
|
205
|
+
soundPlayer.Sound.Parent = parent
|
|
206
|
+
end))
|
|
207
|
+
|
|
208
|
+
maid:GiveTask(Rx.combineLatest({
|
|
209
|
+
visible = self:ObserveRenderStepped();
|
|
210
|
+
multiplier = self._volumeMultiplier:Observe();
|
|
211
|
+
}):Subscribe(function(state)
|
|
212
|
+
soundPlayer:SetVolumeMultiplier(state.multiplier*state.visible)
|
|
213
|
+
end))
|
|
214
|
+
|
|
215
|
+
maid:GiveTask(self:ObserveVisible():Subscribe(function(isVisible, doNotAnimate)
|
|
216
|
+
soundPlayer:SetVisible(isVisible, doNotAnimate)
|
|
217
|
+
end))
|
|
218
|
+
|
|
219
|
+
maid:GiveTask(function()
|
|
220
|
+
soundPlayer:PromiseHide():Then(function()
|
|
221
|
+
renderMaid:Destroy()
|
|
222
|
+
end)
|
|
223
|
+
end)
|
|
224
|
+
|
|
225
|
+
return maid
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
function LoopedSoundPlayer:SetVolumeMultiplier(volume)
|
|
229
|
+
self._volumeMultiplier.Value = volume
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
function LoopedSoundPlayer:_setupLoopScheduling(soundPlayer, loopSchedule)
|
|
233
|
+
local maid = Maid.new()
|
|
234
|
+
|
|
235
|
+
if loopSchedule.maxLoops then
|
|
236
|
+
local loopCount = 0
|
|
237
|
+
maid:GiveTask(self._currentSoundLooped:Connect(function()
|
|
238
|
+
loopCount = loopCount + 1
|
|
239
|
+
|
|
240
|
+
-- Cancel
|
|
241
|
+
if loopCount > loopSchedule.maxLoops then
|
|
242
|
+
self._currentSoundId.Value = nil
|
|
243
|
+
end
|
|
244
|
+
end))
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
if loopSchedule.loopDelay then
|
|
248
|
+
maid:GiveTask(self._currentSoundLooped:Connect(function()
|
|
249
|
+
local waitTime = SoundLoopScheduleUtils.getWaitTimeSeconds(loopSchedule.loopDelay)
|
|
250
|
+
|
|
251
|
+
soundPlayer.Sound:Pause()
|
|
252
|
+
|
|
253
|
+
maid._scheduled = task.delay(waitTime, function()
|
|
254
|
+
self._currentSoundLoopedAfterDelay:Fire()
|
|
255
|
+
soundPlayer.Sound:Play()
|
|
256
|
+
end)
|
|
257
|
+
end))
|
|
258
|
+
else
|
|
259
|
+
maid:GiveTask(self._currentSoundLooped:Connect(function()
|
|
260
|
+
self._currentSoundLoopedAfterDelay:Fire()
|
|
261
|
+
end))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
return maid
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
function LoopedSoundPlayer:SwapToSamples(soundIdList, loopSchedule)
|
|
268
|
+
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
269
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
270
|
+
|
|
271
|
+
local loopMaid = Maid.new()
|
|
272
|
+
|
|
273
|
+
loopMaid:GiveTask(self:_scheduleFirstPlay(loopSchedule, function()
|
|
274
|
+
local sampler = RandomSampler.new(soundIdList)
|
|
275
|
+
self._currentLoopSchedule.Value = loopSchedule
|
|
276
|
+
self._currentSoundId.Value = sampler:Sample()
|
|
277
|
+
|
|
278
|
+
loopMaid:GiveTask(self._currentSoundLoopedAfterDelay:Connect(function()
|
|
279
|
+
self._currentSoundId.Value = sampler:Sample()
|
|
280
|
+
end))
|
|
281
|
+
end))
|
|
282
|
+
|
|
283
|
+
self._maid._swappingTo = loopMaid
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
function LoopedSoundPlayer:SwapToChoice(soundIdList, loopSchedule)
|
|
287
|
+
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
288
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
289
|
+
|
|
290
|
+
local loopMaid = Maid.new()
|
|
291
|
+
|
|
292
|
+
loopMaid:GiveTask(self:_scheduleFirstPlay(loopSchedule, function()
|
|
293
|
+
self._currentLoopSchedule.Value = loopSchedule
|
|
294
|
+
self._currentSoundId.Value = RandomUtils.choice(soundIdList)
|
|
295
|
+
|
|
296
|
+
loopMaid:GiveTask(self._currentSoundLoopedAfterDelay:Connect(function()
|
|
297
|
+
self._currentSoundId.Value = RandomUtils.choice(soundIdList)
|
|
298
|
+
end))
|
|
299
|
+
end))
|
|
300
|
+
|
|
301
|
+
self._maid._swappingTo = loopMaid
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
function LoopedSoundPlayer:PlayOnce(soundId, loopSchedule)
|
|
305
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
306
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
307
|
+
|
|
308
|
+
self:Swap(soundId, SoundLoopScheduleUtils.maxLoops(1, loopSchedule))
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
function LoopedSoundPlayer:SwapOnLoop(soundId, loopSchedule)
|
|
312
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
313
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
314
|
+
|
|
315
|
+
self:Swap(soundId, SoundLoopScheduleUtils.onNextLoop(loopSchedule))
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
function LoopedSoundPlayer:PlayOnceOnLoop(soundId, loopSchedule)
|
|
319
|
+
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
320
|
+
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
321
|
+
|
|
322
|
+
self:PlayOnce(soundId, SoundLoopScheduleUtils.onNextLoop(loopSchedule))
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
function LoopedSoundPlayer:_convertToLoopedSchedule(loopSchedule)
|
|
326
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(loopSchedule) or loopSchedule == nil, "Bad loopSchedule")
|
|
327
|
+
return loopSchedule or self._defaultScheduleOptions
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
function LoopedSoundPlayer:_scheduleFirstPlay(loopSchedule, callback)
|
|
331
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(loopSchedule), "Bad loopSchedule")
|
|
332
|
+
assert(type(callback) == "function", "Bad callback")
|
|
333
|
+
|
|
334
|
+
local maid = Maid.new()
|
|
335
|
+
|
|
336
|
+
local observable = Rx.of(true)
|
|
337
|
+
if loopSchedule.playOnNextLoop then
|
|
338
|
+
observable = observable:Pipe({
|
|
339
|
+
Rx.switchMap(function()
|
|
340
|
+
local waitTime = nil
|
|
341
|
+
if loopSchedule.maxInitialWaitTimeForNextLoop then
|
|
342
|
+
waitTime = SoundLoopScheduleUtils.getWaitTimeSeconds(loopSchedule.maxInitialWaitTimeForNextLoop)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
return self:_observeActiveSoundFinishLoop(waitTime)
|
|
346
|
+
end);
|
|
347
|
+
});
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
if loopSchedule.initialDelay then
|
|
351
|
+
observable = observable:Pipe({
|
|
352
|
+
Rx.switchMap(function()
|
|
353
|
+
return Rx.delayed(SoundLoopScheduleUtils.getWaitTimeSeconds(loopSchedule.initialDelay))
|
|
354
|
+
end);
|
|
355
|
+
});
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
-- Immediate
|
|
359
|
+
if observable then
|
|
360
|
+
maid._observeOnce = observable:Subscribe(function()
|
|
361
|
+
maid._observeOnce = nil
|
|
362
|
+
callback()
|
|
363
|
+
end)
|
|
364
|
+
else
|
|
365
|
+
callback()
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
return maid
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
function LoopedSoundPlayer:StopAfterLoop()
|
|
372
|
+
local swapMaid = Maid.new()
|
|
373
|
+
|
|
374
|
+
swapMaid:GiveTask(self._currentSoundLooped:Connect(function()
|
|
375
|
+
if self._maid._swappingTo == swapMaid then
|
|
376
|
+
self._currentSoundId.Value = nil
|
|
377
|
+
end
|
|
378
|
+
end))
|
|
379
|
+
|
|
380
|
+
self._maid._swappingTo = swapMaid
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
function LoopedSoundPlayer:_observeActiveSoundFinishLoop(maxWaitTime)
|
|
384
|
+
local startTime = os.clock()
|
|
385
|
+
|
|
386
|
+
return self._currentActiveSound:Observe():Pipe({
|
|
387
|
+
Rx.throttleDefer();
|
|
388
|
+
Rx.switchMap(function(sound)
|
|
389
|
+
if not sound then
|
|
390
|
+
return Rx.of(true)
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
return Rx.combineLatest({
|
|
394
|
+
timeLength = RxInstanceUtils.observeProperty(sound, "TimeLength");
|
|
395
|
+
timePosition = RxInstanceUtils.observeProperty(sound, "TimePosition");
|
|
396
|
+
crossFadeTime = self._crossFadeTime:Observe();
|
|
397
|
+
}):Pipe({
|
|
398
|
+
Rx.switchMap(function(state)
|
|
399
|
+
local timeElapsed = os.clock() - startTime
|
|
400
|
+
local timeRemaining
|
|
401
|
+
if maxWaitTime then
|
|
402
|
+
timeRemaining = maxWaitTime - timeElapsed
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
-- We assume it's gonna load
|
|
406
|
+
if state.timeLength == 0 then
|
|
407
|
+
if timeRemaining then
|
|
408
|
+
return Rx.delayed(timeRemaining)
|
|
409
|
+
else
|
|
410
|
+
return Rx.EMPTY
|
|
411
|
+
end
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
local waitTime = state.timeLength - state.timePosition - state.crossFadeTime
|
|
415
|
+
|
|
416
|
+
if timeRemaining then
|
|
417
|
+
waitTime = math.min(waitTime, timeRemaining)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
return Rx.delayed(waitTime)
|
|
421
|
+
end);
|
|
422
|
+
})
|
|
423
|
+
end)
|
|
424
|
+
})
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
function LoopedSoundPlayer:PromiseLoopDone()
|
|
428
|
+
local promise = self._maid:GivePromise(Promise.new())
|
|
429
|
+
|
|
430
|
+
PromiseMaidUtils.whilePromise(promise, function(maid)
|
|
431
|
+
maid:GiveTask(self._currentSoundLooped:Connect(function()
|
|
432
|
+
promise:Resolve()
|
|
433
|
+
end))
|
|
434
|
+
end)
|
|
435
|
+
|
|
436
|
+
return promise
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
function LoopedSoundPlayer:PromiseSustain()
|
|
440
|
+
-- Never resolve (?)
|
|
441
|
+
return Promise.new()
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
function LoopedSoundPlayer:GetSound()
|
|
446
|
+
return self._sound
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
return LoopedSoundPlayer
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class LoopedSoundPlayer.story
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local Maid = require("Maid")
|
|
8
|
+
local LoopedSoundPlayer = require("LoopedSoundPlayer")
|
|
9
|
+
local RandomUtils = require("RandomUtils")
|
|
10
|
+
local Blend = require("Blend")
|
|
11
|
+
local LoopedSoundScheduleUtils = require("LoopedSoundScheduleUtils")
|
|
12
|
+
|
|
13
|
+
return function(target)
|
|
14
|
+
local maid = Maid.new()
|
|
15
|
+
|
|
16
|
+
local ORIGINAL = nil --"rbxassetid://14477435416"
|
|
17
|
+
|
|
18
|
+
local loopedSoundPlayer = LoopedSoundPlayer.new(ORIGINAL, target)
|
|
19
|
+
loopedSoundPlayer:SetDoSyncSoundPlayback(true)
|
|
20
|
+
loopedSoundPlayer:SetCrossFadeTime(2)
|
|
21
|
+
loopedSoundPlayer:SetVolumeMultiplier(0.25)
|
|
22
|
+
loopedSoundPlayer:SetSoundParent(target)
|
|
23
|
+
maid:GiveTask(loopedSoundPlayer)
|
|
24
|
+
|
|
25
|
+
local OPTIONS = {
|
|
26
|
+
"rbxassetid://14477453689";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
maid:GiveTask(task.spawn(function()
|
|
30
|
+
while true do
|
|
31
|
+
task.wait(2)
|
|
32
|
+
-- loopedSoundPlayer:Swap(RandomUtils.choice(OPTIONS))
|
|
33
|
+
end
|
|
34
|
+
end))
|
|
35
|
+
|
|
36
|
+
loopedSoundPlayer:Show()
|
|
37
|
+
|
|
38
|
+
local function button(props)
|
|
39
|
+
return Blend.New "TextButton" {
|
|
40
|
+
Text = props.Text;
|
|
41
|
+
AutoButtonColor = true;
|
|
42
|
+
Font = Enum.Font.FredokaOne;
|
|
43
|
+
Size = UDim2.new(0, 100, 0, 30);
|
|
44
|
+
|
|
45
|
+
Blend.New "UICorner" {
|
|
46
|
+
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
[Blend.OnEvent "Activated"] = function()
|
|
50
|
+
props.OnActivated();
|
|
51
|
+
end;
|
|
52
|
+
};
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
maid:GiveTask(Blend.mount(target, {
|
|
56
|
+
Blend.New "Frame" {
|
|
57
|
+
Name = "ButtonContainer";
|
|
58
|
+
BackgroundTransparency = 1;
|
|
59
|
+
Position = UDim2.new(0.5, 0, 0, 5);
|
|
60
|
+
AnchorPoint = Vector2.new(0.5, 0);
|
|
61
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
62
|
+
|
|
63
|
+
Blend.New "UIListLayout" {
|
|
64
|
+
FillDirection = Enum.FillDirection.Horizontal;
|
|
65
|
+
Padding = UDim.new(0, 5);
|
|
66
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
button({
|
|
70
|
+
Text = "Toggle";
|
|
71
|
+
OnActivated = function()
|
|
72
|
+
loopedSoundPlayer:Toggle()
|
|
73
|
+
end;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
button({
|
|
77
|
+
Text = "Reset";
|
|
78
|
+
OnActivated = function()
|
|
79
|
+
loopedSoundPlayer:Swap(ORIGINAL)
|
|
80
|
+
end;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
button({
|
|
84
|
+
Text = "Swap sample";
|
|
85
|
+
OnActivated = function()
|
|
86
|
+
loopedSoundPlayer:SwapToSamples({
|
|
87
|
+
"rbxassetid://14478670277";
|
|
88
|
+
"rbxassetid://14478671494";
|
|
89
|
+
"rbxassetid://14478672676";
|
|
90
|
+
})
|
|
91
|
+
end;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
button({
|
|
95
|
+
Text = "Play once";
|
|
96
|
+
OnActivated = function()
|
|
97
|
+
loopedSoundPlayer:PlayOnce("rbxassetid://14478764914")
|
|
98
|
+
end;
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
button({
|
|
102
|
+
Text = "Play delayed loop";
|
|
103
|
+
OnActivated = function()
|
|
104
|
+
loopedSoundPlayer:Swap({
|
|
105
|
+
SoundId ="rbxassetid://6052547865";
|
|
106
|
+
Volume = 3;
|
|
107
|
+
}, LoopedSoundScheduleUtils.schedule({
|
|
108
|
+
loopDelay = NumberRange.new(0.25, 1);
|
|
109
|
+
}))
|
|
110
|
+
end;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
button({
|
|
114
|
+
Text = "Swap on loop";
|
|
115
|
+
OnActivated = function()
|
|
116
|
+
loopedSoundPlayer:SwapOnLoop(RandomUtils.choice(OPTIONS))
|
|
117
|
+
end;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
return function()
|
|
123
|
+
maid:DoCleaning()
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class SimpleLoopedSoundPlayer
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local TimedTransitionModel = require("TimedTransitionModel")
|
|
8
|
+
local Rx = require("Rx")
|
|
9
|
+
local SoundUtils = require("SoundUtils")
|
|
10
|
+
local SoundPromiseUtils = require("SoundPromiseUtils")
|
|
11
|
+
local Promise = require("Promise")
|
|
12
|
+
local ValueObject = require("ValueObject")
|
|
13
|
+
|
|
14
|
+
local SimpleLoopedSoundPlayer = setmetatable({}, TimedTransitionModel)
|
|
15
|
+
SimpleLoopedSoundPlayer.ClassName = "SimpleLoopedSoundPlayer"
|
|
16
|
+
SimpleLoopedSoundPlayer.__index = SimpleLoopedSoundPlayer
|
|
17
|
+
|
|
18
|
+
function SimpleLoopedSoundPlayer.new(soundId)
|
|
19
|
+
local self = setmetatable(TimedTransitionModel.new(), SimpleLoopedSoundPlayer)
|
|
20
|
+
|
|
21
|
+
self.Sound = SoundUtils.createSoundFromId(soundId)
|
|
22
|
+
self.Sound.Looped = true
|
|
23
|
+
self.Sound.Archivable = false
|
|
24
|
+
self._maid:GiveTask(self.Sound)
|
|
25
|
+
|
|
26
|
+
self:SetTransitionTime(1)
|
|
27
|
+
|
|
28
|
+
self._volumeMultiplier = ValueObject.new(1, "number")
|
|
29
|
+
self._maid:GiveTask(self._volumeMultiplier)
|
|
30
|
+
|
|
31
|
+
self._maxVolume = self.Sound.Volume
|
|
32
|
+
|
|
33
|
+
self._maid:GiveTask(Rx.combineLatest({
|
|
34
|
+
visible = self:ObserveRenderStepped();
|
|
35
|
+
multiplier = self._volumeMultiplier:Observe();
|
|
36
|
+
}):Subscribe(function(state)
|
|
37
|
+
self.Sound.Volume = state.visible*self._maxVolume*state.multiplier
|
|
38
|
+
end))
|
|
39
|
+
|
|
40
|
+
self._maid:GiveTask(self.VisibleChanged:Connect(function(isVisible)
|
|
41
|
+
if isVisible then
|
|
42
|
+
self.Sound:Play()
|
|
43
|
+
end
|
|
44
|
+
end))
|
|
45
|
+
|
|
46
|
+
return self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
function SimpleLoopedSoundPlayer:SetVolumeMultiplier(volume)
|
|
50
|
+
self._volumeMultiplier.Value = volume
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
function SimpleLoopedSoundPlayer:PromiseSustain()
|
|
54
|
+
-- Never resolve
|
|
55
|
+
return Promise.new()
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
function SimpleLoopedSoundPlayer:PromiseLoopDone()
|
|
59
|
+
return SoundPromiseUtils.promiseLooped(self.Sound)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
return SimpleLoopedSoundPlayer
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
--[[
|
|
2
|
+
@class SimpleLoopedSoundPlayer.story
|
|
3
|
+
]]
|
|
4
|
+
|
|
5
|
+
local require = require(game:GetService("ServerScriptService"):FindFirstChild("LoaderUtils", true).Parent).load(script)
|
|
6
|
+
|
|
7
|
+
local Maid = require("Maid")
|
|
8
|
+
local ServiceBag = require("ServiceBag")
|
|
9
|
+
local SimpleLoopedSoundPlayer = require("SimpleLoopedSoundPlayer")
|
|
10
|
+
local Blend = require("Blend")
|
|
11
|
+
|
|
12
|
+
return function(target)
|
|
13
|
+
local maid = Maid.new()
|
|
14
|
+
local serviceBag = ServiceBag.new()
|
|
15
|
+
maid:GiveTask(serviceBag)
|
|
16
|
+
|
|
17
|
+
local simpleLoopedSoundPlayer = SimpleLoopedSoundPlayer.new("rbxassetid://14477453689")
|
|
18
|
+
simpleLoopedSoundPlayer:SetTransitionTime(1)
|
|
19
|
+
maid:GiveTask(simpleLoopedSoundPlayer)
|
|
20
|
+
|
|
21
|
+
simpleLoopedSoundPlayer.Sound.Parent = target
|
|
22
|
+
|
|
23
|
+
simpleLoopedSoundPlayer:Show()
|
|
24
|
+
|
|
25
|
+
local function button(props)
|
|
26
|
+
return Blend.New "TextButton" {
|
|
27
|
+
Text = props.Text;
|
|
28
|
+
AutoButtonColor = true;
|
|
29
|
+
Font = Enum.Font.FredokaOne;
|
|
30
|
+
Size = UDim2.new(0, 100, 0, 30);
|
|
31
|
+
|
|
32
|
+
Blend.New "UICorner" {
|
|
33
|
+
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
[Blend.OnEvent "Activated"] = function()
|
|
37
|
+
props.OnActivated();
|
|
38
|
+
end;
|
|
39
|
+
};
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
maid:GiveTask(Blend.mount(target, {
|
|
43
|
+
Blend.New "Frame" {
|
|
44
|
+
Name = "ButtonContainer";
|
|
45
|
+
BackgroundTransparency = 1;
|
|
46
|
+
Position = UDim2.new(0.5, 0, 0, 5);
|
|
47
|
+
AnchorPoint = Vector2.new(0.5, 0);
|
|
48
|
+
Size = UDim2.new(1, 0, 0, 30);
|
|
49
|
+
|
|
50
|
+
Blend.New "UIListLayout" {
|
|
51
|
+
FillDirection = Enum.FillDirection.Horizontal;
|
|
52
|
+
Padding = UDim.new(0, 5);
|
|
53
|
+
HorizontalAlignment = Enum.HorizontalAlignment.Center;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
button({
|
|
57
|
+
Text = "Toggle";
|
|
58
|
+
OnActivated = function()
|
|
59
|
+
simpleLoopedSoundPlayer:Toggle()
|
|
60
|
+
end;
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
return function()
|
|
66
|
+
maid:DoCleaning()
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--[=[
|
|
2
|
+
@class SoundLoopScheduleUtils
|
|
3
|
+
]=]
|
|
4
|
+
|
|
5
|
+
local require = require(script.Parent.loader).load(script)
|
|
6
|
+
|
|
7
|
+
local t = require("t")
|
|
8
|
+
local NumberRangeUtils = require("NumberRangeUtils")
|
|
9
|
+
local Table = require("Table")
|
|
10
|
+
|
|
11
|
+
local SoundLoopScheduleUtils = {}
|
|
12
|
+
|
|
13
|
+
function SoundLoopScheduleUtils.schedule(loopedSchedule)
|
|
14
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(loopedSchedule))
|
|
15
|
+
|
|
16
|
+
return table.freeze(loopedSchedule)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
function SoundLoopScheduleUtils.onNextLoop(loopedSchedule)
|
|
20
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(loopedSchedule) or loopedSchedule == nil, "Bad loopedSchedule")
|
|
21
|
+
|
|
22
|
+
loopedSchedule = loopedSchedule or {}
|
|
23
|
+
return SoundLoopScheduleUtils.schedule(Table.merge(loopedSchedule, {
|
|
24
|
+
playOnNextLoop = true;
|
|
25
|
+
}))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
function SoundLoopScheduleUtils.maxLoops(maxLoops, loopedSchedule)
|
|
29
|
+
assert(type(maxLoops) == "number", "Bad maxLoops")
|
|
30
|
+
assert(SoundLoopScheduleUtils.isLoopedSchedule(loopedSchedule) or loopedSchedule == nil, "Bad loopedSchedule")
|
|
31
|
+
|
|
32
|
+
loopedSchedule = loopedSchedule or {}
|
|
33
|
+
return SoundLoopScheduleUtils.schedule(Table.merge(loopedSchedule, {
|
|
34
|
+
maxLoops = maxLoops;
|
|
35
|
+
}))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
function SoundLoopScheduleUtils.default()
|
|
39
|
+
return SoundLoopScheduleUtils.schedule({})
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
SoundLoopScheduleUtils.isWaitTimeSeconds = t.union(t.number, t.NumberRange)
|
|
43
|
+
|
|
44
|
+
SoundLoopScheduleUtils.isLoopedSchedule = t.interface({
|
|
45
|
+
playOnNextLoop = t.optional(t.boolean);
|
|
46
|
+
maxLoops = t.optional(t.number);
|
|
47
|
+
initialDelay = t.optional(SoundLoopScheduleUtils.isWaitTimeSeconds);
|
|
48
|
+
loopDelay = t.optional(SoundLoopScheduleUtils.isWaitTimeSeconds);
|
|
49
|
+
maxInitialWaitTimeForNextLoop = t.optional(SoundLoopScheduleUtils.isWaitTimeSeconds);
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
function SoundLoopScheduleUtils.getWaitTimeSeconds(waitTime)
|
|
53
|
+
assert(SoundLoopScheduleUtils.isWaitTimeSeconds(waitTime))
|
|
54
|
+
|
|
55
|
+
if type(waitTime) == "number" then
|
|
56
|
+
return waitTime
|
|
57
|
+
elseif typeof(waitTime) == "NumberRange" then
|
|
58
|
+
return NumberRangeUtils.getValue(waitTime, math.random())
|
|
59
|
+
else
|
|
60
|
+
error("Bad waitTime")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
return SoundLoopScheduleUtils
|