@quenty/soundplayer 7.30.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 CHANGED
@@ -3,6 +3,23 @@
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
+ ## [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
+
6
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quenty/soundplayer",
3
- "version": "7.30.0",
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.0",
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.0",
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": "8d2e7927e4399b4297a04d474d29bdd85692051d"
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
- _layerMaid: Maid.Maid,
29
- _soundParent: ValueObject.ValueObject<Instance?>,
30
- _soundGroup: ValueObject.ValueObject<SoundGroup?>,
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
- _layers: { [string]: LoopedSoundPlayer.LoopedSoundPlayer },
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._layerMaid = self._maid:Add(Maid.new())
50
-
51
- self._soundParent = self._maid:Add(ValueObject.new(nil, t.optional(t.Instance)))
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._layers = {}
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._bpm:Mount(bpm)
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._soundParent.Value = soundParent
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._soundGroup:Mount(soundGroup)
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:_getOrCreateLayer(layerId)
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:_getOrCreateLayer(layerId)
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:_getOrCreateLayer(layerId)
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:_getOrCreateLayer(layerId)
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:_getOrCreateLayer(layerId)
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:_getOrCreateLayer(layerId)
211
+ local layer = self._layeredSoundHelper:GetOrCreateLayer(layerId)
207
212
  layer:PlayOnceOnLoop(soundId, scheduleOptions)
208
213
  end
209
214
 
210
- function LayeredLoopedSoundPlayer._getOrCreateLayer(
215
+ function LayeredLoopedSoundPlayer._handleNewLayer(
211
216
  self: LayeredLoopedSoundPlayer,
212
- layerId: string
213
- ): LoopedSoundPlayer.LoopedSoundPlayer
214
- if self._layers[layerId] then
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._bpm:Observe():Subscribe(function(bpm)
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(self: LayeredLoopedSoundPlayer, layerId: string): ()
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._layerMaid[layerId] = nil
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
- self._layerMaid:DoCleaning()
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._currentActiveSound = self._maid:Add(ValueObject.new(nil))
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
- assert(type(bpm) == "number" or bpm == nil, "Bad bpm")
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.Value = parent
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) or soundId == nil, "Bad 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
 
@@ -18,6 +18,7 @@ return function(target)
18
18
  simpleLoopedSoundPlayer:SetTransitionTime(1)
19
19
 
20
20
  simpleLoopedSoundPlayer.Sound.Parent = target
21
+ simpleLoopedSoundPlayer.Sound:Play()
21
22
 
22
23
  simpleLoopedSoundPlayer:Show()
23
24
 
@@ -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