Sounds goes silence after some conditions or behavior I caused? More details look at below:
Scripts:
_G.RX_STOP = true task.wait(0.3) _G.RX_STOP = false local ID = "rbxassetid://8169240213" local RPM, INTERVAL = 720, 60/720 local folder = workspace.Terrain:FindFirstChild("RXLegacy") if folder then folder:Destroy() end folder = Instance.new("Folder") folder.Name = "RXLegacy" folder.Parent = workspace.Terrain local part = Instance.new("Part") part.Anchored = true part.CanCollide = false part.Transparency = 1 part.Size = Vector3.one part.Position = Vector3.zero part.Parent = folder local n = 0 task.spawn(function() while not _G.RX_STOP do n += 1 local s = Instance.new("Sound") s.SoundId = ID s.Volume = 0.5 s.RollOffMaxDistance = 2000 s.RollOffMinDistance = 40 s.Parent = part s:Play() s.Ended:Connect(function() s:Destroy() end) task.delay(15, function() if s.Parent then s:Destroy() end end) task.wait(INTERVAL) end folder:Destroy() print("[RXLegacy] stopped after " .. n .. " plays") end) print("[RXLegacy] LEGACY Sound API, same asset & rate — does this stutter/go silent too?")
_G.RX_STOP = true task.wait(0.2) _G.RX_STOP = false local ID = “rbxassetid://129597576449946” local RPM = 720 local INTERVAL = 60/RPM local folder = workspace.Terrain:FindFirstChild(“RXTest”) if folder then folder:Destroy() end folder = Instance.new(“Folder”) folder.Name = “RXTest” folder.Parent = workspace.Terrain local n = 0 task.spawn(function() while not _G.RX_STOP do n += 1 local att = Instance.new(“Attachment”) att.Parent = folder local p = Instance.new(“AudioPlayer”) p.AssetId = ID p.Volume = 0.5 p.Parent = att local em = Instance.new(“AudioEmitter”) em.Parent = att local w = Instance.new(“Wire”) w.SourceInstance = p w.SourceName = “Output” w.TargetInstance = em w.TargetName = “Input” w.Parent = att p:Play() p.Ended:Connect(function() att:Destroy() end) task.delay(15, function() if att.Parent then att:Destroy() end end) task.wait(INTERVAL) end folder:Destroy() print(“[RXTest] stopped after " .. n .. " shots”) end) print(“[RXTest] DIFFERENT ASSET — fire your gun and listen to whether this one survives”)
So I used sound legacy, and new sound API. It seems like no matter what I did, it goes silence. After rapid firing it goes silence.
I found the issue , but not the solution.
Both legacy and new audio API has the same issue when it’s wired with runtime reverb.
But plain audio works correctly
Hey @RobloxStudioLife5 – I tried the provided scripts, and added some reverb via ReverbSoundEffect and AudioReverb, but I was not able to reproduce this behavior.
Do you have an estimate how many Sounds/AudioPlayers are trying to play (i.e. IsPlaying == true) when the dropouts occur?
I’m surprised this post hasn’t gotten more attention, myself and several other players experience this exact same effect in multiple other games, most prominently in our case Deepwoken.
It’s not every game, but its more than just one. I don’t have the scripts to share reasoning, but I can at least confirm it’s not just you.
Can you share any Microprofile captures from during the dropouts? Perhaps something is stalling the audio engine, and we’d see a function that’s normally quick taking a really long time.
Maybe not directly related to the bug report, these scripts are framerate-dependent, since
task.wait(INTERVAL)
waits at leastINTERVAL seconds – the script will resume on the next frame after the elapsed wait-time. That means any really good lag spike could delay audio playback.
You can pre-schedule playback with the SoundService:GetMixerTime() method to decouple audio playback from the framerate – the reworked AudioPlayer script might look something like
_G.RX_STOP = true
task.wait(0.2)
_G.RX_STOP = false
local ID = "rbxassetid://129597576449946"
local RPM = 720
local INTERVAL = 60/RPM
local folder = workspace.Terrain:FindFirstChild("RXTest")
if folder then folder:Destroy() end
folder = Instance.new("Folder")
folder.Name = "RXTest"
folder.Parent = workspace.Terrain
local n = 0
local SoundService = game:GetService("SoundService")
local planAhead = 0.5
task.spawn(function()
local beginTime = SoundService:GetMixerTime()
while not _G.RX_STOP do
n += 1
local att = Instance.new("Attachment")
att.Parent = folder
local p = Instance.new("AudioPlayer")
p.Asset = ID
p.Volume = 0.5
p.Parent = att
local em = Instance.new("AudioEmitter")
em.Parent = att
local w = Instance.new("Wire")
w.SourceInstance = p
w.TargetInstance = em
w.Parent = att
local scheduledTime = beginTime + INTERVAL * n
p.Ended:Connect(function() att:Destroy() end)
p:Play(scheduledTime)
while scheduledTime > SoundService:GetMixerTime() + planAhead do
task.wait()
end
end
folder:Destroy()
print("[RXTest] stopped after " .. n .. " shots")
end)
print("[RXTest] DIFFERENT ASSET — fire your gun and listen to whether this one survives")
Regarding the sound engine I made, it’s a custom sound engine that handles reverb. This is how it wires it:
--!strict
--!native
--!optimize 2
local MAX_PER_PRESET = 48
local ROOM_REVERB_ENABLED = true
export type PoolEntry = {
Attachment : Attachment,
Player : AudioPlayer,
Emitter : AudioEmitter,
Eq : AudioEqualizer,
PlayerWire : Wire,
EqWire : Wire,
Reverb : AudioReverb?,
ReverbWire : Wire?,
Release : (() -> ())?,
}
export type SoundPool = typeof(setmetatable({} :: {
_pools : { [string]: { PoolEntry } },
_destroyed : boolean,
_reverb : Instance?,
}, {} :: { __index: any }))
local SoundPool = {}
SoundPool.__index = SoundPool
SoundPool.__type = "SoundPool"
function SoundPool.new(reverb: Instance?): SoundPool
return setmetatable({
_pools = {} :: { [string]: { PoolEntry } },
_destroyed = false,
_reverb = reverb,
_reverbs = {} :: { AudioReverb },
}, SoundPool) :: any
end
function SoundPool.ApplyRoom(self: SoundPool, apply: (AudioReverb) -> ())
local reverbs = self._reverbs
local count = #reverbs
local live = 0
for i = 1, count do
local reverb = reverbs[i]
if reverb.Parent ~= nil then
live += 1
reverbs[live] = reverb
apply(reverb)
end
end
for i = count, live + 1, -1 do
reverbs[i] = nil
end
end
function SoundPool.CountReverbs(self: SoundPool): number
return #self._reverbs
end
local CURVE_SAMPLES = 12
local function ApplyMaxDistance(emitter: AudioEmitter, maxDistance: number, minDistance: number?)
local near = math.max(minDistance or 10, 0.1)
local far = math.max(maxDistance, near * 2)
local curve = { [0] = 1, [near] = 1 }
local ratio = far / near
for i = 1, CURVE_SAMPLES do
local distance = near * ratio ^ (i / CURVE_SAMPLES)
curve[distance] = near / distance
end
curve[far] = 0
emitter:SetDistanceAttenuation(curve)
end
local function Connect(parent: Instance, name: string, source: Instance, target: Instance): Wire
local wire = Instance.new("Wire")
wire.Name = name
wire.SourceInstance = source
wire.SourceName = "Output"
wire.TargetInstance = target
wire.TargetName = "Input"
wire.Parent = parent
return wire
end
local function InsertReverbBeforeEmitter(attachment: Attachment, emitter: AudioEmitter): (AudioReverb?, Wire?)
local feed: Wire? = nil
local dangling: Wire? = nil
for _, child in attachment:GetDescendants() do
if child:IsA("Wire") then
if child.TargetInstance == emitter then
feed = child
break
elseif not child.Connected and child.SourceInstance ~= nil then
local target = child.TargetInstance
if target == nil or target.Parent == nil then
dangling = dangling or child
end
end
end
end
if not feed and dangling then
warn(string.format(
"SoundPool: '%s' has a Wire ('%s') whose target no longer exists — its authored chain "
.. "never reached the AudioEmitter and the sound would be silent. Reconnecting it to the emitter.",
attachment:GetFullName(),
dangling.Name
))
dangling.TargetInstance = emitter
dangling.TargetName = "Input"
feed = dangling
end
if not feed then return nil, nil end
local tail = feed.SourceInstance
if not tail then return nil, nil end
local reverb = Instance.new("AudioReverb")
reverb.Name = "ResonixRoomReverb"
reverb.DecayTime = 0.01
reverb.Density = 0
reverb.Diffusion = 0
reverb.WetLevel = -80
reverb.Parent = attachment
feed.TargetInstance = reverb
feed.Name = "ResonixTailToReverb"
local outWire = Connect(attachment, "ResonixReverbToEmitter", reverb, emitter)
return reverb, outWire
end
local function CreateEntryFromTemplate(config: any, reverb: Instance?): PoolEntry
local attachment = Instance.new("Attachment")
attachment.Name = "ResonixAudioEmitter"
attachment.Parent = workspace.Terrain
local clone = (config.Template :: Instance):Clone()
local player = clone:FindFirstChildWhichIsA("AudioPlayer", true) :: AudioPlayer
local emitter = clone:FindFirstChildWhichIsA("AudioEmitter", true) :: AudioEmitter
local eq = clone:FindFirstChildWhichIsA("AudioEqualizer", true) :: AudioEqualizer?
assert(player, "SoundPool: Template is missing an AudioPlayer")
assert(emitter, "SoundPool: Template is missing an AudioEmitter")
for _, child in clone:GetChildren() do
child.Parent = attachment
end
clone:Destroy()
if config.MaxDistance ~= nil then
ApplyMaxDistance(emitter, config.MaxDistance, config.MinDistance)
end
if config.Volume ~= nil then
player.Volume = config.Volume
end
player.Looping = false
local entryReverb, reverbWire = nil, nil
if reverb and ROOM_REVERB_ENABLED then
entryReverb, reverbWire = InsertReverbBeforeEmitter(attachment, emitter)
end
if #emitter:GetConnectedWires("Input") == 0 then
warn(string.format(
"SoundPool: '%s' AudioEmitter has no inbound Wire — its Template's chain is broken "
.. "and this sound will be silent.",
attachment:GetFullName()
))
end
if not eq then
warn(string.format(
"SoundPool: '%s' Template has no AudioEqualizer — adding one so material "
.. "transmission filtering has somewhere to land.",
attachment:GetFullName()
))
local added = Instance.new("AudioEqualizer")
added.Name = "ResonixAudioEqualizer"
added.LowGain = 0
added.MidGain = 0
added.HighGain = 0
added.Parent = attachment
eq = added
end
return {
Attachment = attachment,
Player = player,
Emitter = emitter,
Eq = eq :: any,
PlayerWire = nil :: any,
EqWire = nil :: any,
Reverb = entryReverb,
ReverbWire = reverbWire,
}
end
local function CreateEntry(config: any, reverb: Instance?): PoolEntry
if config.Template then
return CreateEntryFromTemplate(config, reverb)
end
local attachment = Instance.new("Attachment")
attachment.Name = "ResonixAudioEmitter"
attachment.Parent = workspace.Terrain
local player = Instance.new("AudioPlayer")
player.Name = "ResonixAudioPlayer"
if config.AudioPlayer then
player.AssetId = config.AudioPlayer.AssetId
else
player.AssetId = config.SoundId or ""
end
player.Looping = false
player.Parent = attachment
local emitter = Instance.new("AudioEmitter")
emitter.Name = "ResonixAudioEmitter"
ApplyMaxDistance(emitter, config.MaxDistance or 100, config.MinDistance)
emitter.Parent = attachment
local eq = Instance.new("AudioEqualizer")
eq.Name = "ResonixAudioEqualizer"
eq.LowGain = 0
eq.MidGain = 0
eq.HighGain = 0
eq.Parent = attachment
local playerWire = Connect(attachment, "ResonixPlayerToEq", player, eq)
local eqWire = Connect(attachment, "ResonixEqToEmitter", eq, emitter)
local entryReverb, reverbWire = nil, nil
if reverb and ROOM_REVERB_ENABLED then
entryReverb, reverbWire = InsertReverbBeforeEmitter(attachment, emitter)
end
if config.Volume ~= nil then
player.Volume = config.Volume
else
player.Volume = 1
end
return {
Attachment = attachment,
Player = player,
Emitter = emitter,
Eq = eq,
PlayerWire = playerWire,
EqWire = eqWire,
Reverb = entryReverb,
ReverbWire = reverbWire,
}
end
function SoundPool.Acquire(
self : SoundPool,
presetKey : string,
config : any,
position : Vector3
): PoolEntry
local pool = self._pools[presetKey]
local entry: PoolEntry
if pool and #pool > 0 then
entry = table.remove(pool) :: PoolEntry
else
entry = CreateEntry(config, self._reverb)
if entry.Reverb then
table.insert(self._reverbs, entry.Reverb)
end
end
entry.Attachment.WorldPosition = position
entry.Attachment.Parent = workspace.Terrain
entry.Release = nil
local rp = config.RandomPitch
if rp then
entry.Player.PlaybackSpeed = rp.Min + math.random() * (rp.Max - rp.Min)
else
entry.Player.PlaybackSpeed = config.PlaybackSpeed or 1
end
return entry
end
function SoundPool.Release(
self : SoundPool,
presetKey : string,
entry : PoolEntry
)
if self._destroyed then
entry.Attachment:Destroy()
return
end
local pool = self._pools[presetKey]
if not pool then
pool = {}
self._pools[presetKey] = pool
end
for _, pooled in pool do
if pooled == entry then
return
end
end
if #pool < MAX_PER_PRESET then
table.insert(pool, entry)
else
entry.Attachment:Destroy()
end
end
function SoundPool.Destroy(self: SoundPool)
if self._destroyed then return end
self._destroyed = true
for _, pool in self._pools do
for _, entry in pool do
entry.Attachment:Destroy()
end
end
table.clear(self._pools)
table.clear(self._reverbs)
end
return SoundPool
I only switched on and off this variable by the way in both videos.
In Addition, This is what happens on Reverb + MixerTime
Code:
--!strict
--!native
--!optimize 2
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local SoundService = game:GetService("SoundService")
assert(RunService:IsClient(), "ResonixAudio.Client must only be required on the client")
local AudioPresets = require(script.Parent.AudioPresets)
local SoundPool = require(script.Parent.SoundPool)
local AcousticEstimator = require(script.Parent.Occupancy.AcousticEstimator)
type RoomParams = AcousticEstimator.RoomParams
type AudioConfig = AudioPresets.AudioConfig
export type ResonixAudioClient = typeof(setmetatable({} :: {
_net : any,
_resonix : any,
_pool : any,
_configs : { [string]: AudioConfig },
_categories : { [string]: { string } },
_connections : { any },
_listenerPositionProvider: () -> Vector3?,
_occupancyGrid : any?,
_active : { [string]: { [any]: boolean } },
_lastScheduled : { [string]: number },
_reverb : AudioReverb?,
_currentRoom : RoomParams?,
_targetRoom : RoomParams?,
_lastVoxelX : number,
_lastVoxelY : number,
_lastVoxelZ : number,
_lastRoomUpdate : number,
_lerpStartClock : number,
_destroyed : boolean,
}, {} :: { __index: any }))
local ResonixAudioClient = {}
ResonixAudioClient.__index = ResonixAudioClient
ResonixAudioClient.__type = "ResonixAudioClient"
local DEFAULT_FREQUENCY = "mid"
local ROOM_UPDATE_THROTTLE = 0.25
local ROOM_LERP_DURATION = 0.4
local MIXER_LEAD = 0.05
local MIXER_MIN_SPACING = 1 / 240
local PLAYBACK_GRACE = 2
local PLAYBACK_POLL = 0.25
local LOAD_TIMEOUT = 10
local OUTDOOR_ROOM: RoomParams = {
DecayTime = 0,
Density = 0,
Diffusion = 0,
WetLevel = 0,
IsEnclosed = false,
}
local function LerpRoom(a: RoomParams, b: RoomParams, t: number): RoomParams
return {
DecayTime = a.DecayTime + (b.DecayTime - a.DecayTime) * t,
Density = a.Density + (b.Density - a.Density) * t,
Diffusion = a.Diffusion + (b.Diffusion - a.Diffusion) * t,
WetLevel = a.WetLevel + (b.WetLevel - a.WetLevel) * t,
IsEnclosed = t >= 1 and b.IsEnclosed or (a.IsEnclosed or b.IsEnclosed),
}
end
local function DefaultListenerPosition(): Vector3?
local character = Players.LocalPlayer and Players.LocalPlayer.Character
if character then
local root = character:FindFirstChild("HumanoidRootPart") :: BasePart?
if root then
return root.Position
end
end
local camera = workspace.CurrentCamera
if camera then
return camera.CFrame.Position
end
return nil
end
local function ApplyTransmission(
entry : any,
config : AudioConfig,
effectiveVolume : number,
materialTable : { low: number, mid: number, high: number }?
)
local baseVolume = config.Volume or 1
entry.Player.Volume = baseVolume * effectiveVolume
if materialTable then
entry.Eq.LowGain = -(materialTable.low * 80)
entry.Eq.MidGain = -(materialTable.mid * 80)
entry.Eq.HighGain = -(materialTable.high * 80)
else
entry.Eq.LowGain = 0
entry.Eq.MidGain = 0
entry.Eq.HighGain = 0
end
end
local WET_LEVEL_DB_MAX = 0
local WET_LEVEL_DB_MIN = -36
local RESONIX_FORCE_WET = false
local function ApplyRoomToReverb(reverb: any, room: RoomParams)
if RESONIX_FORCE_WET then
reverb.DecayTime = 4
reverb.Density = 1
reverb.Diffusion = 1
reverb.WetLevel = WET_LEVEL_DB_MAX
return
end
reverb.DecayTime = math.max(room.DecayTime, 0.01)
reverb.Density = room.Density
reverb.Diffusion = room.Diffusion
reverb.WetLevel = WET_LEVEL_DB_MIN + (WET_LEVEL_DB_MAX - WET_LEVEL_DB_MIN) * room.WetLevel
end
local function NextMixerTime(self: any, presetKey: string): number?
local ok, now = pcall(function()
return SoundService:GetMixerTime()
end)
if not ok or typeof(now) ~= "number" then
return nil
end
local scheduled = now + MIXER_LEAD
local last = self._lastScheduled[presetKey]
if last and scheduled <= last + MIXER_MIN_SPACING then
scheduled = last + MIXER_MIN_SPACING
end
self._lastScheduled[presetKey] = scheduled
return scheduled
end
local function PlayAt(
self : any,
presetKey : string,
config : AudioConfig,
position : Vector3,
source : Instance?
)
local entry = self._pool:Acquire(presetKey, config, position)
if entry.Reverb and self._targetRoom then
ApplyRoomToReverb(entry.Reverb, self._targetRoom :: RoomParams)
end
local resonix = self._resonix
if resonix then
local listenerPos = self._listenerPositionProvider()
if listenerPos then
local preset = resonix:GetPresets()[presetKey]
if preset then
local excludeList = {}
if source then
excludeList[1] = source
end
local _, transmission, materialTable = resonix.SoundPropagator.ComputeTransmission(
position,
listenerPos,
preset.Frequency or DEFAULT_FREQUENCY,
preset.Radius,
preset.Intensity,
excludeList,
nil,
preset.Diffraction
)
ApplyTransmission(entry, config, transmission, materialTable)
end
end
end
local scheduledTime = NextMixerTime(self, presetKey)
if scheduledTime then
entry.Player:Play(scheduledTime)
else
entry.Player:Play()
end
local active = self._active[presetKey]
if not active then
active = {}
self._active[presetKey] = active
end
active[entry] = true
local released = false
local endedConn: RBXScriptConnection? = nil
local function release()
if released then return end
released = true
if endedConn then
endedConn:Disconnect()
endedConn = nil
end
local stillActive = self._active[presetKey]
if stillActive then
stillActive[entry] = nil
end
self._pool:Release(presetKey, entry)
end
endedConn = entry.Player.Ended:Connect(release)
entry.Release = release
task.spawn(function()
local startedPlaying = false
local waited = 0
-- A scheduled voice is not IsPlaying until its timestamp arrives, so the
-- load window has to start from then rather than from Play(). Without
-- this the lead time counts against LOAD_TIMEOUT, and a voice queued far
-- enough ahead would be reclaimed before the mixer ever reached it.
local graceUntil = scheduledTime and (scheduledTime + MIXER_LEAD) or nil
while not released do
task.wait(PLAYBACK_POLL)
if released then return end
if graceUntil then
local ok, now = pcall(function()
return SoundService:GetMixerTime()
end)
if ok and typeof(now) == "number" and now < graceUntil then
continue
end
graceUntil = nil
end
waited += PLAYBACK_POLL
local player = entry.Player
if player.IsPlaying then
startedPlaying = true
elseif startedPlaying then
task.wait(PLAYBACK_GRACE)
if not released and not player.IsPlaying then
release()
end
return
elseif waited >= LOAD_TIMEOUT then
release()
return
end
end
end)
end
local function GetOrCreateDeviceOutput(): AudioDeviceOutput
local existing = SoundService:FindFirstChildOfClass("AudioDeviceOutput")
if existing then
return existing :: AudioDeviceOutput
end
local output = Instance.new("AudioDeviceOutput")
output.Name = "ResonixAudioOutput"
output.Parent = SoundService
return output
end
local function InitRoomState(self: any)
self._currentRoom = OUTDOOR_ROOM
self._targetRoom = OUTDOOR_ROOM
self._lastVoxelX = math.huge
self._lastVoxelY = math.huge
self._lastVoxelZ = math.huge
self._lastRoomUpdate = 0
self._lerpStartClock = 0
end
local function UpdateRoom(self: any)
local grid = self._occupancyGrid
if not grid then return end
local now = os.clock()
if now - self._lastRoomUpdate < ROOM_UPDATE_THROTTLE then
return
end
self._lastRoomUpdate = now
local listenerPos = self._listenerPositionProvider()
if not listenerPos then return end
local lvx, lvy, lvz = grid:WorldToVoxel(listenerPos)
if lvx ~= self._lastVoxelX or lvy ~= self._lastVoxelY or lvz ~= self._lastVoxelZ then
self._lastVoxelX = lvx
self._lastVoxelY = lvy
self._lastVoxelZ = lvz
local resonix = self._resonix
local newRoom: RoomParams
if resonix then
newRoom = AcousticEstimator.EstimateRoom(grid, lvx, lvy, lvz, resonix.SoundPropagator)
else
newRoom = OUTDOOR_ROOM
end
-- Start the new blend from what is actually being heard right now, not
-- from the previous target. Voxel crossings come faster than a full
-- lerp while walking, so using the target would snap to a room the
-- listener never reached.
local elapsed = ROOM_LERP_DURATION > 0
and math.clamp((now - self._lerpStartClock) / ROOM_LERP_DURATION, 0, 1)
or 1
self._currentRoom = LerpRoom(self._currentRoom :: RoomParams, self._targetRoom :: RoomParams, elapsed)
self._targetRoom = newRoom
self._lerpStartClock = now
end
local t = ROOM_LERP_DURATION > 0
and math.clamp((now - self._lerpStartClock) / ROOM_LERP_DURATION, 0, 1)
or 1
local blended = LerpRoom(self._currentRoom :: RoomParams, self._targetRoom :: RoomParams, t)
self._pool:ApplyRoom(function(reverb: AudioReverb)
ApplyRoomToReverb(reverb, blended)
end)
end
function ResonixAudioClient.new(
resonixNetClient : any,
resonix : any?,
occupancyGrid : any?,
customPresets : { [string]: AudioConfig }?
): ResonixAudioClient
local configs: { [string]: AudioConfig } = {}
for key, cfg in AudioPresets do
configs[key] = cfg
end
if customPresets then
for key, cfg in customPresets do
configs[key] = cfg
end
end
local reverbMarker = occupancyGrid and GetOrCreateDeviceOutput() or nil
local self = setmetatable({
_net = resonixNetClient,
_resonix = resonix,
_pool = SoundPool.new(reverbMarker),
_configs = configs,
_categories = {} :: { [string]: { string } },
_connections = {} :: { any },
_listenerPositionProvider = DefaultListenerPosition,
_occupancyGrid = occupancyGrid,
_active = {} :: { [string]: { [any]: boolean } },
_lastScheduled = {} :: { [string]: number },
_destroyed = false,
}, ResonixAudioClient) :: any
if occupancyGrid then
InitRoomState(self)
local heartbeatConn = RunService.Heartbeat:Connect(function()
UpdateRoom(self)
end)
table.insert(self._connections, heartbeatConn)
end
local emitConn = resonixNetClient.OnEmitReceived:Connect(function(
_serverEmissionId : number,
presetKey : string,
position : Vector3
)
local config = self._configs[presetKey]
if not config then
return
end
PlayAt(self, presetKey, config, position, nil)
end)
table.insert(self._connections, emitConn)
return self
end
function ResonixAudioClient.SetListenerPositionProvider(
self : ResonixAudioClient,
Fn : (() -> Vector3?)?
)
assert(not self._destroyed, "ResonixAudioClient.SetListenerPositionProvider: handle is destroyed");
(self :: any)._listenerPositionProvider = Fn or DefaultListenerPosition
end
function ResonixAudioClient.RegisterAudio(
self : ResonixAudioClient,
presetKey : string,
config : AudioConfig
)
assert(not self._destroyed, "ResonixAudioClient.RegisterAudio: handle is destroyed")
assert(presetKey and presetKey ~= "", "RegisterAudio: presetKey is required")
local hasSoundId = config.SoundId and config.SoundId ~= ""
local hasAudioPlayer = typeof(config.AudioPlayer) == "Instance" and config.AudioPlayer:IsA("AudioPlayer")
local hasTemplate = typeof(config.Template) == "Instance"
if not (hasSoundId or hasAudioPlayer or hasTemplate) then
warn("RegisterAudio: '" .. presetKey .. "' has none of SoundId (string), AudioPlayer (AudioPlayer instance), or Template (Instance), skipping")
return
end
self._configs[presetKey] = config
end
function ResonixAudioClient.PlayLocal(
self : ResonixAudioClient,
presetKey : string,
position : Vector3
)
assert(not self._destroyed, "ResonixAudioClient.PlayLocal: handle is destroyed")
local config = self._configs[presetKey]
if not config then return end
PlayAt(self, presetKey, config, position, nil)
end
-- Cuts every currently-playing instance of a preset. Stopping an AudioPlayer
-- does not raise Ended, so the pooled entries are released here directly.
function ResonixAudioClient.StopLocal(
self : ResonixAudioClient,
presetKey : string
)
assert(not self._destroyed, "ResonixAudioClient.StopLocal: handle is destroyed")
local active = self._active[presetKey]
if not active then return end
for entry in active do
entry.Player:Stop()
if entry.Release then
entry.Release()
else
self._pool:Release(presetKey, entry)
end
end
self._active[presetKey] = nil
self._lastScheduled[presetKey] = nil
end
function ResonixAudioClient.RegisterCategory(
self : ResonixAudioClient,
categoryKey : string,
presetKeys : { string }
)
assert(not self._destroyed, "ResonixAudioClient.RegisterCategory: handle is destroyed")
self._categories[categoryKey] = presetKeys
end
function ResonixAudioClient.PlayLocalCategory(
self : ResonixAudioClient,
categoryKey : string,
position : Vector3
)
assert(not self._destroyed, "ResonixAudioClient.PlayLocalCategory: handle is destroyed")
local presetKeys = self._categories[categoryKey]
if not presetKeys or #presetKeys == 0 then return end
local key = presetKeys[math.random(#presetKeys)]
local config = self._configs[key]
if not config then return end
PlayAt(self, key, config, position, nil)
end
function ResonixAudioClient.Destroy(self: ResonixAudioClient)
if self._destroyed then return end
self._destroyed = true
for _, conn in self._connections do
if typeof(conn) == "RBXScriptConnection" then
conn:Disconnect()
elseif typeof(conn) == "table" and conn.Disconnect then
conn:Disconnect()
end
end
table.clear(self._connections)
self._pool:Destroy()
end
return ResonixAudioClient
Video:
The micro profiler seems stable after rapid firing?
Thanks for sharing that code – I will try to reproduce it on my side.
In the after-reverb case, can you click Dump > 512 frames, and share the file that gets generated? It should get put into your logs folder; it’s an html file