@quenty/soundplayer 7.30.0-canary.644.d3040d7.0 → 7.30.1-canary.45a29f2.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 +18 -1
- package/package.json +10 -8
- package/src/Client/Loops/Layered/LayeredLoopedSoundPlayer.lua +49 -56
- package/src/Client/Loops/Layered/LayeredSoundHelper.lua +83 -0
- package/src/Client/Loops/LoopedSoundPlayer.lua +53 -12
- package/src/Client/Loops/SimpleLoopedSoundPlayer.lua +0 -6
- package/src/Client/Loops/SimpleLoopedSoundPlayer.story.lua +1 -0
- package/src/Client/Service/SoundPlayerServiceClient.lua +71 -0
- package/src/Client/Stack/SoundPlayerStack.lua +71 -0
- package/src/Server/SoundPlayerService.lua +27 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,7 +3,24 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
## [7.30.1-canary.45a29f2.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/soundplayer@7.30.0...@quenty/soundplayer@7.30.1-canary.45a29f2.0) (2026-01-18)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* Push sound order priority ([45a29f2](https://github.com/Quenty/NevermoreEngine/commit/45a29f21a00d72d7971299695e28129d5b9d0cfb))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* Make hiding work ([8203ae4](https://github.com/Quenty/NevermoreEngine/commit/8203ae460bd4e5866f7328c54ac48f62addfd637))
|
|
17
|
+
* Route sound player into stack ([8ec445b](https://github.com/Quenty/NevermoreEngine/commit/8ec445bc4d3cd60b247f78b37f19ba902e2e477a))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# [7.30.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/soundplayer@7.29.0...@quenty/soundplayer@7.30.0) (2026-01-16)
|
|
7
24
|
|
|
8
25
|
**Note:** Version bump only for package @quenty/soundplayer
|
|
9
26
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/soundplayer",
|
|
3
|
-
"version": "7.30.
|
|
3
|
+
"version": "7.30.1-canary.45a29f2.0",
|
|
4
4
|
"description": "Sound playback helper",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -30,26 +30,28 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@quenty/adorneeutils": "3.3.5",
|
|
32
32
|
"@quenty/baseobject": "10.9.3",
|
|
33
|
-
"@quenty/blend": "12.27.
|
|
34
|
-
"@quenty/brio": "14.23.0",
|
|
35
|
-
"@quenty/instanceutils": "13.23.0",
|
|
33
|
+
"@quenty/blend": "12.27.1-canary.45a29f2.0",
|
|
34
|
+
"@quenty/brio": "14.23.1-canary.45a29f2.0",
|
|
35
|
+
"@quenty/instanceutils": "13.23.1-canary.45a29f2.0",
|
|
36
36
|
"@quenty/loader": "10.9.3",
|
|
37
37
|
"@quenty/maid": "3.5.3",
|
|
38
38
|
"@quenty/numberrangeutils": "3.1.3",
|
|
39
|
+
"@quenty/observablecollection": "12.29.1-canary.45a29f2.0",
|
|
39
40
|
"@quenty/promise": "10.13.0",
|
|
40
41
|
"@quenty/promisemaid": "5.13.0",
|
|
41
42
|
"@quenty/randomutils": "6.12.2",
|
|
42
43
|
"@quenty/rbxasset": "5.10.3",
|
|
43
|
-
"@quenty/rx": "13.22.0",
|
|
44
|
+
"@quenty/rx": "13.22.1-canary.45a29f2.0",
|
|
44
45
|
"@quenty/signal": "7.11.5",
|
|
46
|
+
"@quenty/soundgroup": "1.31.1-canary.45a29f2.0",
|
|
45
47
|
"@quenty/sounds": "10.16.0",
|
|
46
48
|
"@quenty/table": "3.9.2",
|
|
47
|
-
"@quenty/transitionmodel": "7.29.
|
|
48
|
-
"@quenty/valueobject": "13.24.0",
|
|
49
|
+
"@quenty/transitionmodel": "7.29.1-canary.45a29f2.0",
|
|
50
|
+
"@quenty/valueobject": "13.24.1-canary.45a29f2.0",
|
|
49
51
|
"@quentystudios/t": "^3.0.0"
|
|
50
52
|
},
|
|
51
53
|
"publishConfig": {
|
|
52
54
|
"access": "public"
|
|
53
55
|
},
|
|
54
|
-
"gitHead": "
|
|
56
|
+
"gitHead": "45a29f21a00d72d7971299695e28129d5b9d0cfb"
|
|
55
57
|
}
|
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
local require = require(script.Parent.loader).load(script)
|
|
11
11
|
|
|
12
|
+
local LayeredSoundHelper = require("LayeredSoundHelper")
|
|
12
13
|
local LoopedSoundPlayer = require("LoopedSoundPlayer")
|
|
13
14
|
local Maid = require("Maid")
|
|
15
|
+
local Promise = require("Promise")
|
|
16
|
+
local PromiseUtils = require("PromiseUtils")
|
|
14
17
|
local Rx = require("Rx")
|
|
15
18
|
local SoundLoopScheduleUtils = require("SoundLoopScheduleUtils")
|
|
16
19
|
local SoundUtils = require("SoundUtils")
|
|
@@ -25,13 +28,13 @@ LayeredLoopedSoundPlayer.__index = LayeredLoopedSoundPlayer
|
|
|
25
28
|
export type LayeredLoopedSoundPlayer =
|
|
26
29
|
typeof(setmetatable(
|
|
27
30
|
{} :: {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
_bpm: ValueObject.ValueObject<number?>,
|
|
31
|
+
_defaultSoundParent: ValueObject.ValueObject<Instance?>,
|
|
32
|
+
_defaultSoundGroup: ValueObject.ValueObject<SoundGroup?>,
|
|
33
|
+
_defaultBPM: ValueObject.ValueObject<number?>,
|
|
32
34
|
_defaultCrossFadeTime: ValueObject.ValueObject<number>,
|
|
33
35
|
_volumeMultiplier: ValueObject.ValueObject<number>,
|
|
34
|
-
|
|
36
|
+
|
|
37
|
+
_layeredSoundHelper: LayeredSoundHelper.LayeredSoundHelper<LoopedSoundPlayer.LoopedSoundPlayer>,
|
|
35
38
|
},
|
|
36
39
|
{} :: typeof({ __index = LayeredLoopedSoundPlayer })
|
|
37
40
|
))
|
|
@@ -46,15 +49,17 @@ export type LayeredLoopedSoundPlayer =
|
|
|
46
49
|
function LayeredLoopedSoundPlayer.new(soundParent: Instance?): LayeredLoopedSoundPlayer
|
|
47
50
|
local self: LayeredLoopedSoundPlayer = setmetatable(SpringTransitionModel.new() :: any, LayeredLoopedSoundPlayer)
|
|
48
51
|
|
|
49
|
-
self.
|
|
50
|
-
|
|
51
|
-
self.
|
|
52
|
-
self._soundGroup = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance)))
|
|
53
|
-
self._bpm = self._maid:Add(ValueObject.new(nil, t.optional(t.number)))
|
|
52
|
+
self._defaultSoundParent = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance)))
|
|
53
|
+
self._defaultSoundGroup = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance)))
|
|
54
|
+
self._defaultBPM = self._maid:Add(ValueObject.new(nil, t.optional(t.number)))
|
|
54
55
|
self._defaultCrossFadeTime = self._maid:Add(ValueObject.new(0.5, "number"))
|
|
55
56
|
self._volumeMultiplier = self._maid:Add(ValueObject.new(1, "number"))
|
|
56
57
|
|
|
57
|
-
self.
|
|
58
|
+
self._layeredSoundHelper = self._maid:Add(LayeredSoundHelper.new(function(maid): any
|
|
59
|
+
local layer = maid:Add(LoopedSoundPlayer.new())
|
|
60
|
+
self:_handleNewLayer(maid, layer)
|
|
61
|
+
return layer
|
|
62
|
+
end))
|
|
58
63
|
|
|
59
64
|
if soundParent then
|
|
60
65
|
self:SetSoundParent(soundParent)
|
|
@@ -87,7 +92,7 @@ end
|
|
|
87
92
|
Sets the BPM for syncing sound playback.
|
|
88
93
|
]=]
|
|
89
94
|
function LayeredLoopedSoundPlayer.SetBPM(self: LayeredLoopedSoundPlayer, bpm: ValueObject.Mountable<number?>): () -> ()
|
|
90
|
-
return self.
|
|
95
|
+
return self._defaultBPM:Mount(bpm)
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
--[=[
|
|
@@ -96,7 +101,7 @@ end
|
|
|
96
101
|
function LayeredLoopedSoundPlayer.SetSoundParent(self: LayeredLoopedSoundPlayer, soundParent: Instance?): ()
|
|
97
102
|
assert(typeof(soundParent) == "Instance" or soundParent == nil, "Bad soundParent")
|
|
98
103
|
|
|
99
|
-
self.
|
|
104
|
+
self._defaultSoundParent.Value = soundParent
|
|
100
105
|
end
|
|
101
106
|
|
|
102
107
|
--[=[
|
|
@@ -106,7 +111,7 @@ function LayeredLoopedSoundPlayer.SetSoundGroup(
|
|
|
106
111
|
self: LayeredLoopedSoundPlayer,
|
|
107
112
|
soundGroup: ValueObject.Mountable<SoundGroup?>
|
|
108
113
|
): () -> ()
|
|
109
|
-
return self.
|
|
114
|
+
return self._defaultSoundGroup:Mount(soundGroup)
|
|
110
115
|
end
|
|
111
116
|
|
|
112
117
|
--[=[
|
|
@@ -122,7 +127,7 @@ function LayeredLoopedSoundPlayer.Swap(
|
|
|
122
127
|
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
123
128
|
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
124
129
|
|
|
125
|
-
local layer = self:
|
|
130
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
126
131
|
layer:Swap(soundId, scheduleOptions)
|
|
127
132
|
end
|
|
128
133
|
|
|
@@ -138,7 +143,7 @@ function LayeredLoopedSoundPlayer.SwapOnLoop(
|
|
|
138
143
|
assert(type(layerId) == "string", "Bad layerId")
|
|
139
144
|
assert(SoundUtils.isConvertableToRbxAsset(soundId) or soundId == nil, "Bad soundId")
|
|
140
145
|
|
|
141
|
-
local layer = self:
|
|
146
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
142
147
|
layer:SwapOnLoop(soundId, scheduleOptions)
|
|
143
148
|
end
|
|
144
149
|
|
|
@@ -155,7 +160,7 @@ function LayeredLoopedSoundPlayer.SwapToSamples(
|
|
|
155
160
|
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
156
161
|
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
157
162
|
|
|
158
|
-
local layer = self:
|
|
163
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
159
164
|
layer:SwapToSamples(soundIdList, scheduleOptions)
|
|
160
165
|
end
|
|
161
166
|
|
|
@@ -172,7 +177,7 @@ function LayeredLoopedSoundPlayer.SwapToChoice(
|
|
|
172
177
|
assert(type(soundIdList) == "table", "Bad soundIdList")
|
|
173
178
|
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
174
179
|
|
|
175
|
-
local layer = self:
|
|
180
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
176
181
|
layer:SwapToChoice(soundIdList, scheduleOptions)
|
|
177
182
|
end
|
|
178
183
|
|
|
@@ -188,7 +193,7 @@ function LayeredLoopedSoundPlayer.PlayOnce(
|
|
|
188
193
|
assert(type(layerId) == "string", "Bad layerId")
|
|
189
194
|
assert(SoundLoopScheduleUtils.isLoopedSchedule(scheduleOptions) or scheduleOptions == nil, "Bad scheduleOptions")
|
|
190
195
|
|
|
191
|
-
local layer = self:
|
|
196
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
192
197
|
layer:PlayOnce(soundId, scheduleOptions)
|
|
193
198
|
end
|
|
194
199
|
|
|
@@ -203,41 +208,25 @@ function LayeredLoopedSoundPlayer.PlayOnceOnLoop(
|
|
|
203
208
|
): ()
|
|
204
209
|
assert(type(layerId) == "string", "Bad layerId")
|
|
205
210
|
|
|
206
|
-
local layer = self:
|
|
211
|
+
local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
|
|
207
212
|
layer:PlayOnceOnLoop(soundId, scheduleOptions)
|
|
208
213
|
end
|
|
209
214
|
|
|
210
|
-
function LayeredLoopedSoundPlayer.
|
|
215
|
+
function LayeredLoopedSoundPlayer._handleNewLayer(
|
|
211
216
|
self: LayeredLoopedSoundPlayer,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return self._layers[layerId]
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
local maid = Maid.new()
|
|
219
|
-
|
|
220
|
-
local layer = maid:Add(LoopedSoundPlayer.new())
|
|
217
|
+
maid: Maid.Maid,
|
|
218
|
+
layer: LoopedSoundPlayer.LoopedSoundPlayer
|
|
219
|
+
): ()
|
|
221
220
|
layer:SetDoSyncSoundPlayback(true)
|
|
222
|
-
|
|
223
|
-
maid:GiveTask(self._soundGroup:Observe():Subscribe(function(soundGroup)
|
|
224
|
-
layer:SetSoundGroup(soundGroup)
|
|
225
|
-
end))
|
|
226
|
-
|
|
221
|
+
maid:GiveTask(layer:SetSoundGroup(self._defaultSoundGroup:Observe()))
|
|
227
222
|
maid:GiveTask(layer:SetCrossFadeTime(self._defaultCrossFadeTime:Observe()))
|
|
228
|
-
|
|
229
|
-
maid:GiveTask(self.
|
|
230
|
-
layer:SetBPM(bpm)
|
|
231
|
-
end))
|
|
223
|
+
maid:GiveTask(layer:SetBPM(self._defaultBPM:Observe()))
|
|
224
|
+
maid:GiveTask(layer:SetSoundParent(self._defaultSoundParent:Observe()))
|
|
232
225
|
|
|
233
226
|
maid:GiveTask(self:ObserveVisible():Subscribe(function(isVisible, doNotAnimate)
|
|
234
227
|
layer:SetVisible(isVisible, doNotAnimate)
|
|
235
228
|
end))
|
|
236
229
|
|
|
237
|
-
maid:GiveTask(self._soundParent:Observe():Subscribe(function(parent)
|
|
238
|
-
layer:SetSoundParent(parent)
|
|
239
|
-
end))
|
|
240
|
-
|
|
241
230
|
maid:GiveTask(Rx.combineLatest({
|
|
242
231
|
visible = self:ObserveRenderStepped(),
|
|
243
232
|
multiplier = self._volumeMultiplier:Observe(),
|
|
@@ -245,29 +234,33 @@ function LayeredLoopedSoundPlayer._getOrCreateLayer(
|
|
|
245
234
|
layer:SetVolumeMultiplier(state.multiplier * state.visible)
|
|
246
235
|
end))
|
|
247
236
|
|
|
248
|
-
self._layers[layerId] = layer
|
|
249
|
-
maid:GiveTask(function()
|
|
250
|
-
if self._layers[layerId] == layer then
|
|
251
|
-
self._layers[layerId] = nil
|
|
252
|
-
end
|
|
253
|
-
end)
|
|
254
|
-
|
|
255
|
-
self._layerMaid[layerId] = maid
|
|
256
|
-
|
|
257
237
|
return layer
|
|
258
238
|
end
|
|
259
239
|
|
|
260
240
|
--[=[
|
|
261
241
|
Stops playback on the given layer.
|
|
262
242
|
]=]
|
|
263
|
-
function LayeredLoopedSoundPlayer.StopLayer(
|
|
243
|
+
function LayeredLoopedSoundPlayer.StopLayer(
|
|
244
|
+
self: LayeredLoopedSoundPlayer,
|
|
245
|
+
layerId: string,
|
|
246
|
+
doNotAnimate: boolean?
|
|
247
|
+
): Promise.Promise<()>
|
|
264
248
|
assert(type(layerId) == "string", "Bad layerId")
|
|
265
249
|
|
|
266
|
-
self.
|
|
250
|
+
local found = self._layeredSoundHelper:FindLayer(layerId)
|
|
251
|
+
if not found then
|
|
252
|
+
return Promise.resolved()
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
return found:PromiseHide(doNotAnimate)
|
|
267
256
|
end
|
|
268
257
|
|
|
269
|
-
function LayeredLoopedSoundPlayer.StopAll(self: LayeredLoopedSoundPlayer): ()
|
|
270
|
-
|
|
258
|
+
function LayeredLoopedSoundPlayer.StopAll(self: LayeredLoopedSoundPlayer, doNotAnimate: boolean?): ()
|
|
259
|
+
local promises: { Promise.Promise<()> } = {}
|
|
260
|
+
for _, layer: any in self._layeredSoundHelper:GetAllLayers() do
|
|
261
|
+
table.insert(promises, layer:PromiseHide(doNotAnimate))
|
|
262
|
+
end
|
|
263
|
+
return PromiseUtils.all(promises)
|
|
271
264
|
end
|
|
272
265
|
|
|
273
266
|
return LayeredLoopedSoundPlayer
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class LayeredSoundHelper
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local BaseObject = require("BaseObject")
|
|
9
|
+
local Maid = require("Maid")
|
|
10
|
+
|
|
11
|
+
local LayeredSoundHelper = setmetatable({}, BaseObject)
|
|
12
|
+
LayeredSoundHelper.ClassName = "LayeredSoundHelper"
|
|
13
|
+
LayeredSoundHelper.__index = LayeredSoundHelper
|
|
14
|
+
|
|
15
|
+
export type CreateSoundPlayer<T> = (maid: Maid.Maid, layerId: string) -> T
|
|
16
|
+
|
|
17
|
+
export type LayeredSoundHelper<T> =
|
|
18
|
+
typeof(setmetatable(
|
|
19
|
+
{} :: {
|
|
20
|
+
_layerMaid: Maid.Maid,
|
|
21
|
+
_createSoundPlayer: CreateSoundPlayer<T>,
|
|
22
|
+
_layers: { [string]: T },
|
|
23
|
+
},
|
|
24
|
+
{} :: typeof({ __index = LayeredSoundHelper })
|
|
25
|
+
))
|
|
26
|
+
& BaseObject.BaseObject
|
|
27
|
+
|
|
28
|
+
function LayeredSoundHelper.new<T>(createSoundPlayer: CreateSoundPlayer<T>): LayeredSoundHelper<T>
|
|
29
|
+
local self: LayeredSoundHelper<T> = setmetatable(BaseObject.new() :: any, LayeredSoundHelper)
|
|
30
|
+
|
|
31
|
+
self._createSoundPlayer = assert(createSoundPlayer, "No createSoundPlayer")
|
|
32
|
+
|
|
33
|
+
self._layerMaid = self._maid:Add(Maid.new())
|
|
34
|
+
self._layers = {}
|
|
35
|
+
|
|
36
|
+
return self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
function LayeredSoundHelper.GetOrCreateLayer<T>(self: LayeredSoundHelper<T>, layerId: string): T
|
|
40
|
+
if self._layers[layerId] then
|
|
41
|
+
return self._layers[layerId]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
local maid = Maid.new()
|
|
45
|
+
|
|
46
|
+
local layer = maid:Add(self._createSoundPlayer(maid, layerId))
|
|
47
|
+
|
|
48
|
+
self._layers[layerId] = layer
|
|
49
|
+
maid:GiveTask(function()
|
|
50
|
+
if self._layers[layerId] == layer then
|
|
51
|
+
self._layers[layerId] = nil
|
|
52
|
+
end
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
self._layerMaid[layerId] = maid
|
|
56
|
+
|
|
57
|
+
-- Generic typing wasn't happy with enforcing inheritance
|
|
58
|
+
maid:GiveTask((layer :: any).HidingComplete:Connect(function()
|
|
59
|
+
self._layerMaid[layerId] = nil
|
|
60
|
+
end))
|
|
61
|
+
|
|
62
|
+
return layer
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
function LayeredSoundHelper.FindLayer<T>(self: LayeredSoundHelper<T>, layerId: string): T?
|
|
66
|
+
return self._layers[layerId]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
function LayeredSoundHelper.GetAllLayers<T>(self: LayeredSoundHelper<T>): { [string]: T }
|
|
70
|
+
return self._layers
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
function LayeredSoundHelper.RemovePlayer<T>(self: LayeredSoundHelper<T>, layerId: string): ()
|
|
74
|
+
assert(type(layerId) == "string", "Bad layerId")
|
|
75
|
+
|
|
76
|
+
self._layerMaid[layerId] = nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
function LayeredSoundHelper.RemoveAllPlayers<T>(self: LayeredSoundHelper<T>): ()
|
|
80
|
+
self._layerMaid:DoCleaning()
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
return LayeredSoundHelper
|
|
@@ -22,11 +22,18 @@ local SoundPromiseUtils = require("SoundPromiseUtils")
|
|
|
22
22
|
local SoundUtils = require("SoundUtils")
|
|
23
23
|
local SpringTransitionModel = require("SpringTransitionModel")
|
|
24
24
|
local ValueObject = require("ValueObject")
|
|
25
|
+
local t = require("t")
|
|
25
26
|
|
|
26
27
|
local LoopedSoundPlayer = setmetatable({}, SpringTransitionModel)
|
|
27
28
|
LoopedSoundPlayer.ClassName = "LoopedSoundPlayer"
|
|
28
29
|
LoopedSoundPlayer.__index = LoopedSoundPlayer
|
|
29
30
|
|
|
31
|
+
type TimePositionData = {
|
|
32
|
+
SoundId: SoundUtils.SoundId,
|
|
33
|
+
TimePosition: number,
|
|
34
|
+
Timestamp: number,
|
|
35
|
+
}
|
|
36
|
+
|
|
30
37
|
export type LoopedSoundPlayer =
|
|
31
38
|
typeof(setmetatable(
|
|
32
39
|
{} :: {
|
|
@@ -38,10 +45,12 @@ export type LoopedSoundPlayer =
|
|
|
38
45
|
_crossFadeTime: ValueObject.ValueObject<number>,
|
|
39
46
|
_volumeMultiplier: ValueObject.ValueObject<number>,
|
|
40
47
|
_doSyncSoundPlayback: ValueObject.ValueObject<boolean>,
|
|
48
|
+
_restoreTimePosition: ValueObject.ValueObject<boolean>,
|
|
41
49
|
_currentActiveSound: ValueObject.ValueObject<Sound?>,
|
|
42
50
|
_currentSoundId: ValueObject.ValueObject<SoundUtils.SoundId?>,
|
|
43
51
|
_defaultScheduleOptions: SoundLoopScheduleUtils.SoundLoopSchedule,
|
|
44
52
|
_currentLoopSchedule: ValueObject.ValueObject<SoundLoopScheduleUtils.SoundLoopSchedule>,
|
|
53
|
+
_lastTimePositionData: TimePositionData?,
|
|
45
54
|
},
|
|
46
55
|
{} :: typeof({ __index = LoopedSoundPlayer })
|
|
47
56
|
))
|
|
@@ -57,14 +66,16 @@ function LoopedSoundPlayer.new(soundId: SoundUtils.SoundId?, soundParent: Instan
|
|
|
57
66
|
|
|
58
67
|
self:SetSpeed(10)
|
|
59
68
|
|
|
60
|
-
self._bpm = self._maid:Add(ValueObject.new(nil))
|
|
61
|
-
self._soundParent = self._maid:Add(ValueObject.new(nil))
|
|
62
|
-
self._soundGroup = self._maid:Add(ValueObject.new(nil))
|
|
69
|
+
self._bpm = self._maid:Add(ValueObject.new(nil, t.optional(t.number :: any)))
|
|
70
|
+
self._soundParent = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance :: any)))
|
|
71
|
+
self._soundGroup = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance :: any)))
|
|
63
72
|
self._crossFadeTime = self._maid:Add(ValueObject.new(0.5, "number"))
|
|
64
73
|
self._volumeMultiplier = self._maid:Add(ValueObject.new(1, "number"))
|
|
65
74
|
self._doSyncSoundPlayback = self._maid:Add(ValueObject.new(false, "boolean"))
|
|
66
|
-
self.
|
|
75
|
+
self._restoreTimePosition = self._maid:Add(ValueObject.new(true, "boolean"))
|
|
76
|
+
self._currentActiveSound = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance :: any)))
|
|
67
77
|
self._currentSoundId = self._maid:Add(ValueObject.new(soundId))
|
|
78
|
+
self._lastTimePositionData = nil
|
|
68
79
|
|
|
69
80
|
self._defaultScheduleOptions = SoundLoopScheduleUtils.default()
|
|
70
81
|
self._currentLoopSchedule = self._maid:Add(ValueObject.new(self._defaultScheduleOptions))
|
|
@@ -114,25 +125,23 @@ end
|
|
|
114
125
|
--[=[
|
|
115
126
|
Sets the BPM for syncing sound playback.
|
|
116
127
|
]=]
|
|
117
|
-
function LoopedSoundPlayer.SetBPM(self: LoopedSoundPlayer, bpm: number
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self._bpm.Value = bpm
|
|
128
|
+
function LoopedSoundPlayer.SetBPM(self: LoopedSoundPlayer, bpm: ValueObject.Mountable<number?>): () -> ()
|
|
129
|
+
return self._bpm:Mount(bpm)
|
|
121
130
|
end
|
|
122
131
|
|
|
123
132
|
--[=[
|
|
124
133
|
Sets the parent instance for the sound.
|
|
125
134
|
]=]
|
|
126
|
-
function LoopedSoundPlayer.SetSoundParent(self: LoopedSoundPlayer, parent: Instance
|
|
127
|
-
self._soundParent
|
|
135
|
+
function LoopedSoundPlayer.SetSoundParent(self: LoopedSoundPlayer, parent: ValueObject.Mountable<Instance?>): () -> ()
|
|
136
|
+
return self._soundParent:Mount(parent)
|
|
128
137
|
end
|
|
129
138
|
|
|
130
139
|
function LoopedSoundPlayer.Swap(
|
|
131
140
|
self: LoopedSoundPlayer,
|
|
132
|
-
soundId: SoundUtils.SoundId
|
|
141
|
+
soundId: SoundUtils.SoundId?,
|
|
133
142
|
loopSchedule: SoundLoopScheduleUtils.SoundLoopSchedule?
|
|
134
143
|
): ()
|
|
135
|
-
assert(SoundUtils.isConvertableToRbxAsset(soundId)
|
|
144
|
+
assert(soundId == nil or SoundUtils.isConvertableToRbxAsset(soundId), "Bad soundId")
|
|
136
145
|
loopSchedule = self:_convertToLoopedSchedule(loopSchedule)
|
|
137
146
|
assert(loopSchedule ~= nil, "Bad loopSchedule")
|
|
138
147
|
|
|
@@ -153,6 +162,10 @@ function LoopedSoundPlayer.SetDoSyncSoundPlayback(self: LoopedSoundPlayer, doSyn
|
|
|
153
162
|
self._doSyncSoundPlayback.Value = doSyncSoundPlayback
|
|
154
163
|
end
|
|
155
164
|
|
|
165
|
+
function LoopedSoundPlayer.SetDoRestoreTimePosition(self: LoopedSoundPlayer, doRestoreTimePosition: boolean): ()
|
|
166
|
+
self._restoreTimePosition.Value = doRestoreTimePosition
|
|
167
|
+
end
|
|
168
|
+
|
|
156
169
|
function LoopedSoundPlayer._setupRender(self: LoopedSoundPlayer): ()
|
|
157
170
|
self._maid:GiveTask(self._currentSoundId
|
|
158
171
|
:ObserveBrio(function(value)
|
|
@@ -170,6 +183,18 @@ function LoopedSoundPlayer._setupRender(self: LoopedSoundPlayer): ()
|
|
|
170
183
|
end))
|
|
171
184
|
end
|
|
172
185
|
|
|
186
|
+
function LoopedSoundPlayer._captureTimePositionData(
|
|
187
|
+
self: LoopedSoundPlayer,
|
|
188
|
+
soundId: SoundUtils.SoundId,
|
|
189
|
+
timePosition: number
|
|
190
|
+
): ()
|
|
191
|
+
self._lastTimePositionData = {
|
|
192
|
+
SoundId = soundId,
|
|
193
|
+
TimePosition = timePosition,
|
|
194
|
+
Timestamp = os.clock(),
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
173
198
|
function LoopedSoundPlayer._renderSoundPlayer(self: LoopedSoundPlayer, soundId: SoundUtils.SoundId): Maid.Maid
|
|
174
199
|
local maid = Maid.new()
|
|
175
200
|
|
|
@@ -177,6 +202,7 @@ function LoopedSoundPlayer._renderSoundPlayer(self: LoopedSoundPlayer, soundId:
|
|
|
177
202
|
local soundPlayer: SimpleLoopedSoundPlayer.SimpleLoopedSoundPlayer =
|
|
178
203
|
renderMaid:Add(SimpleLoopedSoundPlayer.new(soundId))
|
|
179
204
|
soundPlayer:SetTransitionTime(self._crossFadeTime)
|
|
205
|
+
soundPlayer.Sound:Play()
|
|
180
206
|
|
|
181
207
|
renderMaid:GiveTask(self._soundGroup:Observe():Subscribe(function(soundGroup)
|
|
182
208
|
soundPlayer:SetSoundGroup(soundGroup)
|
|
@@ -185,11 +211,22 @@ function LoopedSoundPlayer._renderSoundPlayer(self: LoopedSoundPlayer, soundId:
|
|
|
185
211
|
renderMaid:GiveTask(Rx.combineLatest({
|
|
186
212
|
bpm = self._bpm:Observe(),
|
|
187
213
|
isLoaded = Rx.fromPromise(SoundPromiseUtils.promiseLoaded(soundPlayer.Sound)),
|
|
214
|
+
restoreTimePosition = self._restoreTimePosition:Observe(),
|
|
188
215
|
doSyncSoundPlayback = self._doSyncSoundPlayback:Observe(),
|
|
189
216
|
timeLength = RxInstanceUtils.observeProperty(soundPlayer.Sound, "TimeLength"),
|
|
190
217
|
}):Subscribe(function(state: any)
|
|
191
218
|
local syncMaid = Maid.new()
|
|
192
219
|
|
|
220
|
+
if state.restoreTimePosition then
|
|
221
|
+
local data = self._lastTimePositionData
|
|
222
|
+
if data and data.SoundId == soundId then
|
|
223
|
+
local ourFinalPosition = (data.TimePosition + (os.clock() - data.Timestamp)) % state.timeLength
|
|
224
|
+
soundPlayer.Sound.TimePosition = ourFinalPosition
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
self:_captureTimePositionData(soundId, soundPlayer.Sound.TimePosition)
|
|
229
|
+
|
|
193
230
|
if state.doSyncSoundPlayback then
|
|
194
231
|
if state.bpm then
|
|
195
232
|
local bps = state.bpm / 60
|
|
@@ -470,6 +507,10 @@ function LoopedSoundPlayer.StopAfterLoop(self: LoopedSoundPlayer): ()
|
|
|
470
507
|
self._maid._swappingTo = swapMaid
|
|
471
508
|
end
|
|
472
509
|
|
|
510
|
+
function LoopedSoundPlayer.GetCurrentSoundId(self: LoopedSoundPlayer): SoundUtils.SoundId?
|
|
511
|
+
return self._currentSoundId.Value
|
|
512
|
+
end
|
|
513
|
+
|
|
473
514
|
function LoopedSoundPlayer._observeActiveSoundFinishLoop(
|
|
474
515
|
self: LoopedSoundPlayer,
|
|
475
516
|
maxWaitTime: number
|
|
@@ -47,12 +47,6 @@ function SimpleLoopedSoundPlayer.new(soundId: SoundUtils.SoundId): SimpleLoopedS
|
|
|
47
47
|
self.Sound.Volume = state.visible * self._maxVolume * state.multiplier
|
|
48
48
|
end))
|
|
49
49
|
|
|
50
|
-
self._maid:GiveTask(self.VisibleChanged:Connect(function(isVisible)
|
|
51
|
-
if isVisible then
|
|
52
|
-
self.Sound:Play()
|
|
53
|
-
end
|
|
54
|
-
end))
|
|
55
|
-
|
|
56
50
|
return self
|
|
57
51
|
end
|
|
58
52
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class SoundPlayerServiceClient
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local LayeredSoundHelper = require("LayeredSoundHelper")
|
|
9
|
+
local LoopedSoundPlayer = require("LoopedSoundPlayer")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Observable = require("Observable")
|
|
12
|
+
local ServiceBag = require("ServiceBag")
|
|
13
|
+
local SoundPlayerStack = require("SoundPlayerStack")
|
|
14
|
+
|
|
15
|
+
local SoundPlayerServiceClient = {}
|
|
16
|
+
SoundPlayerServiceClient.ServiceName = "SoundPlayerServiceClient"
|
|
17
|
+
|
|
18
|
+
export type SoundPlayerServiceClient = typeof(setmetatable(
|
|
19
|
+
{} :: {
|
|
20
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
21
|
+
_soundGroupService: any,
|
|
22
|
+
_soundPlayerHelper: LayeredSoundHelper.LayeredSoundHelper<SoundPlayerStack.SoundPlayerStack>,
|
|
23
|
+
_maid: Maid.Maid,
|
|
24
|
+
},
|
|
25
|
+
{} :: typeof({ __index = SoundPlayerServiceClient })
|
|
26
|
+
))
|
|
27
|
+
|
|
28
|
+
function SoundPlayerServiceClient.Init(self: SoundPlayerServiceClient, serviceBag: ServiceBag.ServiceBag)
|
|
29
|
+
assert(not (self :: any)._serviceBag, "Already initialized")
|
|
30
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
31
|
+
self._maid = Maid.new()
|
|
32
|
+
|
|
33
|
+
-- External
|
|
34
|
+
self._serviceBag:GetService(require("SoundGroupServiceClient"))
|
|
35
|
+
|
|
36
|
+
self._soundPlayerHelper = self._maid:Add(LayeredSoundHelper.new(function(maid)
|
|
37
|
+
local layer: SoundPlayerStack.SoundPlayerStack = maid:Add(SoundPlayerStack.new())
|
|
38
|
+
|
|
39
|
+
return layer
|
|
40
|
+
end))
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
--[=[
|
|
44
|
+
Gets a sound group player for the given sound group path
|
|
45
|
+
|
|
46
|
+
@param layerId string
|
|
47
|
+
@return SoundPlayerStack
|
|
48
|
+
]=]
|
|
49
|
+
function SoundPlayerServiceClient.GetOrCreateSoundPlayerStack(
|
|
50
|
+
self: SoundPlayerServiceClient,
|
|
51
|
+
layerId: string
|
|
52
|
+
): SoundPlayerStack.SoundPlayerStack
|
|
53
|
+
return self._soundPlayerHelper:GetOrCreateLayer(layerId)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
function SoundPlayerServiceClient.PushSoundPlayer(
|
|
57
|
+
self: SoundPlayerServiceClient,
|
|
58
|
+
layerId: string,
|
|
59
|
+
soundPlayer: LoopedSoundPlayer.LoopedSoundPlayer,
|
|
60
|
+
priority: (number | Observable.Observable<number>)?
|
|
61
|
+
): () -> ()
|
|
62
|
+
local layer = self._soundPlayerHelper:GetOrCreateLayer(layerId)
|
|
63
|
+
return layer:PushSoundPlayer(soundPlayer, priority)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
function SoundPlayerServiceClient.Destroy(self: SoundPlayerServiceClient): ()
|
|
67
|
+
self._maid:DoCleaning()
|
|
68
|
+
self._soundPlayerHelper = nil :: any
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return SoundPlayerServiceClient
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class SoundPlayerStack
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local BaseObject = require("BaseObject")
|
|
9
|
+
local LoopedSoundPlayer = require("LoopedSoundPlayer")
|
|
10
|
+
local Maid = require("Maid")
|
|
11
|
+
local Observable = require("Observable")
|
|
12
|
+
local ObservableSortedList = require("ObservableSortedList")
|
|
13
|
+
local Signal = require("Signal")
|
|
14
|
+
|
|
15
|
+
local SoundPlayerStack = setmetatable({}, BaseObject)
|
|
16
|
+
SoundPlayerStack.ClassName = "SoundPlayerStack"
|
|
17
|
+
SoundPlayerStack.__index = SoundPlayerStack
|
|
18
|
+
|
|
19
|
+
export type SoundPlayerStack =
|
|
20
|
+
typeof(setmetatable(
|
|
21
|
+
{} :: {
|
|
22
|
+
_stack: any, --ObservableSortedList.ObservableSortedList<LoopedSoundPlayer.LoopedSoundPlayer>,
|
|
23
|
+
HidingComplete: Signal.Signal<()>,
|
|
24
|
+
},
|
|
25
|
+
{} :: typeof({ __index = SoundPlayerStack })
|
|
26
|
+
))
|
|
27
|
+
& BaseObject.BaseObject
|
|
28
|
+
|
|
29
|
+
function SoundPlayerStack.new(): SoundPlayerStack
|
|
30
|
+
local self: SoundPlayerStack = setmetatable(BaseObject.new() :: any, SoundPlayerStack)
|
|
31
|
+
|
|
32
|
+
self._stack = self._maid:Add(ObservableSortedList.new())
|
|
33
|
+
|
|
34
|
+
-- Used to notify the LayeredSoundHelper that we've finished hiding
|
|
35
|
+
-- and have no more sound players to play.
|
|
36
|
+
self.HidingComplete = self._maid:Add(Signal.new())
|
|
37
|
+
|
|
38
|
+
-- TODO: connect to visible + fire hiding finished when our stack is empty
|
|
39
|
+
|
|
40
|
+
self._maid:GiveTask(self._stack:ObserveAtIndex(-1):Subscribe(function(soundPlayer)
|
|
41
|
+
if soundPlayer then
|
|
42
|
+
local maid = Maid.new()
|
|
43
|
+
soundPlayer:Show()
|
|
44
|
+
|
|
45
|
+
maid:GiveTask(function()
|
|
46
|
+
if not soundPlayer.Destroy then
|
|
47
|
+
return
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
soundPlayer:Hide()
|
|
51
|
+
end)
|
|
52
|
+
|
|
53
|
+
self._maid._playing = maid
|
|
54
|
+
else
|
|
55
|
+
self._maid._playing = nil
|
|
56
|
+
self.HidingComplete:Fire()
|
|
57
|
+
end
|
|
58
|
+
end))
|
|
59
|
+
|
|
60
|
+
return self
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
function SoundPlayerStack.PushSoundPlayer(
|
|
64
|
+
self: SoundPlayerStack,
|
|
65
|
+
soundPlayer: LoopedSoundPlayer.LoopedSoundPlayer,
|
|
66
|
+
priority: (Observable.Observable<number> | number)?
|
|
67
|
+
): () -> ()
|
|
68
|
+
return self._stack:Add(soundPlayer, priority or 0)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
return SoundPlayerStack
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[=[
|
|
3
|
+
@class SoundPlayerService
|
|
4
|
+
]=]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local ServiceBag = require("ServiceBag")
|
|
9
|
+
|
|
10
|
+
local SoundPlayerService = {}
|
|
11
|
+
SoundPlayerService.ServiceName = "SoundPlayerService"
|
|
12
|
+
|
|
13
|
+
export type SoundPlayerService = typeof(setmetatable(
|
|
14
|
+
{} :: {
|
|
15
|
+
_serviceBag: ServiceBag.ServiceBag,
|
|
16
|
+
},
|
|
17
|
+
{} :: typeof({ __index = SoundPlayerService })
|
|
18
|
+
))
|
|
19
|
+
|
|
20
|
+
function SoundPlayerService.Init(self: SoundPlayerService, serviceBag: ServiceBag.ServiceBag)
|
|
21
|
+
assert(not (self :: any)._serviceBag, "Already initialized")
|
|
22
|
+
self._serviceBag = assert(serviceBag, "No serviceBag")
|
|
23
|
+
|
|
24
|
+
self._serviceBag:GetService(require("SoundGroupService"))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
return SoundPlayerService
|