Why it exists
Give game systems one service for targeted or broadcast playback while the listening client owns its audio graph.
The source handles channel routing, positional emitters, playback handles, and resource cleanup across a server/client boundary.
Based on the Audio System code in the local commons-area repo.
How it works
- 01
Select listeners
AudioService validates a play request and chooses a broadcast or specific players.
- 02
Send packet
A Play packet carries a handle ID, channel, source, options, and optional target or emitter group.
- 03
Build playback
AudioController creates AudioPlayer and AudioFader records, wiring them into a channel mixer or emitter.
- 04
Release resources
The client disconnects Ended, cancels delayed stops, and destroys wires, players, and faders when a record ends.
Selected code
These excerpts are from the paths shown on each patch. Select an annotation for a focused note; copy code directly from each patch.
AudioService.luau
commons-area/src/server/Services/AudioService.luau
Validates a play request, creates a handle, and routes the packet to all or selected players. It is the server half of the audio boundary.
function AudioService:_sendPlay(players: any, channelName: string, source: any, options: any, target: Instance?, targetGroup: string?): string?
profilingService:Increment("Audio.PlayRequests")
if type(channelName) ~= "string" or channelName == "" or source == nil then
profilingService:Increment("Audio.PlayRejected")
return nil
end
local resolvedOptions = self:_buildOptions(options)
local handleId = self:_getHandleId(resolvedOptions.HandleId)
resolvedOptions.Channel = nil
resolvedOptions.HandleId = nil
resolvedOptions.Target = nil
resolvedOptions.TargetGroup = nil
local payload = {
HandleId = handleId,
Channel = channelName,
Source = source,
Options = resolvedOptions,
Target = target,
TargetGroup = targetGroup,
}
if players == nil then
profilingService:Increment("Audio.BroadcastPlayRequests")
packets.Audio.Play.sendToAll(payload)
return handleId
end
local recipients = self:_getPlayers(players)
if #recipients == 0 then
profilingService:Increment("Audio.PlayRejected")
return nil
end
profilingService:Increment("Audio.TargetedPlayRequests")
profilingService:Increment("Audio.PlayRecipients", #recipients)
for _, player in recipients do
packets.Audio.Play.sendTo(payload, player)
end
return handleId
endAudioController.luau
commons-area/src/client/Controllers/AudioController.luau
Tears down one playback record, including its connection, delayed task, wires, player, and fader. This makes handle cleanup explicit.
function AudioController:_destroyRecord(record: any): ()
if record.Destroyed then
return
end
record.Destroyed = true
if record.Ended then
record.Ended:Disconnect()
record.Ended = nil
end
if record.StopTask then
self:_cancelDelayedTask(record.StopTask)
record.StopTask = nil
end
for _, wire in record.Wires do
if wire.Parent ~= nil then
wire:Destroy()
end
end
if record.Player.Parent ~= nil then
record.Player:Stop()
record.Player:Destroy()
end
if record.Fader.Parent ~= nil then
record.Fader:Destroy()
end
endEngineering decisions
Server chooses recipients
- Reason
- The server service owns broadcast and targeted packet delivery.
- Alternative
- Each feature sends and manages its own playback messages.
- Tradeoff
- AudioService becomes the shared routing boundary.
Client owns the graph
- Reason
- Per-listener AudioPlayer, AudioFader, emitter, and wire lifetimes stay with playback.
- Alternative
- Leave instances to incidental cleanup.
- Tradeoff
- The controller must maintain handles and teardown paths.
Outcome & next steps
Outcome
The inspected source implements shared packet routing and client-owned playback records with explicit teardown.
What improved
The source keeps listener selection separate from per-client audio graph lifetimes.
Current limitations
- This audit did not include an in-experience audio test or recording.
- Individual contribution within the collaborative repository was not independently verified.
Future improvements
- Add a runtime capture and tests for playback routing and teardown.
What this demonstrates
Luau service boundaries, packet delivery, graph construction, and lifecycle cleanup.