@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 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
+ ```
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "soundplayer",
3
+ "tree": {
4
+ "$path": "src"
5
+ }
6
+ }
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
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "node_modules",
3
+ "globIgnorePaths": [ "**/.package-lock.json" ],
4
+ "tree": {
5
+ "$path": { "optional": "../node_modules" }
6
+ }
7
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "SoundPlayerTest",
3
+ "tree": {
4
+ "$className": "DataModel",
5
+ "ServerScriptService": {
6
+ "soundplayer": {
7
+ "$path": ".."
8
+ }
9
+ }
10
+ }
11
+ }