Hello, everyone.
I’m working on a speaker system using the new Roblox Audio API and I’m running into some unexpected issues.
I’m trying to get my AudioChannelSplitter to work correctly with a stereo audio source, but the output from my speakers remains mono. I’ve been unable to figure out what’s wrong, despite trying several things.
I’ve already:
- Checked multiple online forums.
- Asked for help from other AI assistants (Gemini, ChatGPT).
I suspect I might be wiring something incorrectly, but I’m not sure what it is.
Below is the relevant code. The first script is the main server-side script that creates the audio effects and wires, and the second is a local script that creates the audio output and the AudioChannelSplitter.
Thank you for any help you can provide!
**(Main Script)**
-- | Services |
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- | Configuration & Constants |
local ADMIN_USERS = {
["Emils_Studio"] = true,
["RealFauxPlayzs"] = true,
}
local RESPONSE_PREFIX = "[Sonic Havoc] 🔊 | "
local sonicHavoc = Workspace:WaitForChild("[Sonic Havoc] Workspace")
local worker = sonicHavoc:WaitForChild("Worker")
local config = worker:WaitForChild("Configuration")
local remotes = ReplicatedStorage:WaitForChild("SonicHavocRemotes")
-- | Module Imports |
local AudioSets = require(worker:WaitForChild("AudioSets"))
local Wire = require(worker:WaitForChild("Modules"):WaitForChild("Wire"))
local PRESETS = require(worker:WaitForChild("Modules"):WaitForChild("Presets"))
-- | Folder Setup |
local wiresFolder = sonicHavoc:FindFirstChild("Wires")
local effectWires = wiresFolder:FindFirstChild("Effects")
local speakerWires = wiresFolder:FindFirstChild("Speaker")
-- | Channel Definitions |
local CHANNELS = {
MID_L = { group = config.MID_L.Value, isSub = false, side = "left" },
MID_R = { group = config.MID_R.Value, isSub = false, side = "right" },
SUB = { group = config.SUB.Value, isSub = true, side = "mono" },
}
local masterVolume = 0.5
local audioGraph = { player = nil, masterMixer = nil, splitter = nil, channels = {}, emitters = {}, wires = {}, sound = nil }
local channelState = {}
local function defaultFX(isSub)
return {
eq = {
LowGain = isSub and 1.5 or 0,
MidGain = isSub and 0.5 or 0,
HighGain = isSub and -4 or 0,
MidRange = isSub and 200
},
compressor = {
Threshold = -18,
Ratio = 3.0,
Attack = 0.03,
Release = 0.15,
MakeupGain = 1,
},
limiter = {
MaxLevel = -0.3,
Release = 0.15,
},
reverb = {
Enabled = not isSub,
DryLevel = 0,
WetLevel = -22,
DecayTime = 1.8,
Density = 1,
Diffusion = 1,
DecayRatio = 0.5,
HighCutFrequency = 18000,
},
distortion = {
Enabled = false,
Level = 0.05,
},
echo = {
Enabled = false,
DelayTime = 0.3,
Feedback = 0.3,
WetLevel = -12,
DryLevel = 0,
RampTime = 0,
}
}
end
for key, cfg in pairs(CHANNELS) do
channelState[key] = {
volume = 1.0,
solo = false,
eq = defaultFX(cfg.isSub).eq,
compressor = defaultFX(cfg.isSub).compressor,
limiter = defaultFX(cfg.isSub).limiter,
reverb = defaultFX(cfg.isSub).reverb,
distortion = defaultFX(cfg.isSub).distortion,
echo = defaultFX(cfg.isSub).echo,
}
end
-- | Helper Functions |
local function shallowCopy(t)
if type(t) ~= "table" then return t end
local out = {}
for k, v in pairs(t) do
if type(v) == "table" then
out[k] = shallowCopy(v)
else
out[k] = v
end
end
return out
end
local function setupAudioGraph()
audioGraph.player = sonicHavoc.Audio:FindFirstChild("Player") and sonicHavoc.Audio.Player:FindFirstChild("MasterPlayer")
audioGraph.sound = sonicHavoc.Audio:FindFirstChild("Sound") and sonicHavoc.Audio.Sound:FindFirstChild("audio")
if not audioGraph.player then
warn(RESPONSE_PREFIX .. "AudioPlayer not found! Cannot create audio graph.")
return
end
local effectFolder = sonicHavoc.Audio:FindFirstChild("Effect") or Instance.new("Folder", sonicHavoc.Audio)
effectFolder.Name = "Effect"
local masterEQ = Instance.new("AudioEqualizer")
local masterLimiter = Instance.new("AudioLimiter")
masterEQ.Parent = effectFolder
masterLimiter.Parent = effectFolder
audioGraph.masterEQ = masterEQ
audioGraph.masterLimiter = masterLimiter
local function connect(src, tgt)
if src and tgt then
local w = Wire.new(src, tgt)
if w then
w.Parent = effectWires
table.insert(audioGraph.wires, w)
end
end
end
connect(audioGraph.player, masterEQ)
connect(masterEQ, masterLimiter)
for name, cfg in pairs(CHANNELS) do
local chain = {}
chain.fader = Instance.new("AudioFader")
chain.compressor = Instance.new("AudioCompressor")
chain.crossover = Instance.new("AudioEqualizer")
chain.eq = Instance.new("AudioEqualizer")
chain.distortion = Instance.new("AudioDistortion")
chain.reverb = Instance.new("AudioReverb")
chain.echo = Instance.new("AudioEcho")
chain.limiter = Instance.new("AudioLimiter")
local channelFolder = effectFolder:FindFirstChild(name) or Instance.new("Folder", effectFolder)
channelFolder.Name = name
for _, eff in pairs(chain) do eff.Parent = channelFolder end
connect(masterLimiter, chain.fader)
connect(chain.fader, chain.compressor)
connect(chain.compressor, chain.crossover)
connect(chain.crossover, chain.eq)
connect(chain.eq, chain.distortion)
connect(chain.distortion, chain.reverb)
connect(chain.reverb, chain.echo)
connect(chain.echo, chain.limiter)
audioGraph.channels[name] = chain
audioGraph.emitters[name] = {}
end
-- Attach emitters
for name, cfg in pairs(CHANNELS) do
local chain = audioGraph.channels[name]
local groups = {}
if cfg.isSub then
table.insert(groups, config.SUB.Value)
else
table.insert(groups, cfg.group)
end
for _, group in ipairs(groups) do
for _, model in ipairs(group:GetChildren()) do
local attachment = model:FindFirstChild("Audio") and model.Audio
if attachment then
local emitter = Instance.new("AudioEmitter")
emitter.Name = model.Name.."_Emitter"
emitter.Parent = attachment
local curve = {
[config.AudioMinDistance.Value] = 1,
[config.AudioMaxDistance.Value / 2] = 0.5,
[config.AudioMaxDistance.Value] = 0
}
emitter:SetDistanceAttenuation(curve)
table.insert(audioGraph.emitters[name], emitter)
local emitterWire = Wire.new(chain.limiter, emitter)
if emitterWire then
emitterWire.Parent = speakerWires
table.insert(audioGraph.wires, emitterWire)
end
end
end
end
end
end
local function updateAudioGraph()
if not audioGraph.player then return end
local anySolo = false
for _, st in pairs(channelState) do if st.solo then anySolo = true break end end
local loudnessComp = math.max(0, 3 * (1 - masterVolume))
for name, cfg in pairs(CHANNELS) do
local state = channelState[name]
local chain = audioGraph.channels[name]
if not state or not chain then continue end
local vol = state.volume
if anySolo and not state.solo then vol = 0 end
chain.fader.Volume = math.clamp(vol * masterVolume, 0, 2.0)
chain.compressor.Bypass = false
chain.compressor.Threshold = state.compressor.Threshold
chain.compressor.Ratio = state.compressor.Ratio
chain.compressor.Attack = state.compressor.Attack
chain.compressor.Release = state.compressor.Release
chain.compressor.MakeupGain = state.compressor.MakeupGain
if cfg.isSub then
chain.crossover.HighGain, chain.crossover.MidGain, chain.crossover.LowGain = -80, -12, 0
chain.crossover.MidRange = NumberRange.new(200, 200)
else
chain.crossover.HighGain, chain.crossover.MidGain, chain.crossover.LowGain = 0, 0, -80
end
-- Distortion
chain.distortion.Bypass = not state.distortion.Enabled
chain.distortion.Level = state.distortion.Level * (0.8 + state.volume * 0.2)
-- EQ
chain.eq.LowGain = state.eq.LowGain + (cfg.isSub and 0 or loudnessComp)
chain.eq.MidGain = state.eq.MidGain
chain.eq.HighGain = state.eq.HighGain + (cfg.isSub and 0 or loudnessComp * 0.7)
-- Reverb
chain.reverb.Bypass = not state.reverb.Enabled
chain.reverb.DryLevel = state.reverb.DryLevel
chain.reverb.WetLevel = state.reverb.WetLevel
chain.reverb.DecayTime = state.reverb.DecayTime
chain.reverb.Density = state.reverb.Density
chain.reverb.Diffusion = state.reverb.Diffusion
chain.reverb.DecayRatio = state.reverb.DecayRatio
chain.reverb.HighCutFrequency = state.reverb.HighCutFrequency
-- Echo
chain.echo.Bypass = not state.echo.Enabled
chain.echo.DelayTime = state.echo.DelayTime
chain.echo.Feedback = state.echo.Feedback
chain.echo.WetLevel = state.echo.WetLevel
chain.echo.DryLevel = state.echo.DryLevel
chain.echo.RampTime = state.echo.RampTime
-- Limiter
chain.limiter.Bypass = false
chain.limiter.MaxLevel = state.limiter.MaxLevel
chain.limiter.Release = state.limiter.Release
end
end
local function handleCommand(player, cmd, args)
cmd = cmd:lower()
local sub = args[1] and args[1]:lower()
if cmd == "/play" and sub == "set" then
local setName = args[2]
local setData = AudioSets.Sets[setName]
if not setData or not setData.Tracks or #setData.Tracks == 0 then
print(RESPONSE_PREFIX.."Set not found or empty.")
return
end
local assetId = tostring(setData.Tracks[1])
pcall(function()
audioGraph.player.AssetId = "rbxassetid://" .. assetId
audioGraph.player.PlaybackSpeed = setData.Speed or 1
audioGraph.player.TimePosition = 0
audioGraph.player:Play()
if audioGraph.sound then
audioGraph.sound.SoundId = "rbxassetid://" .. assetId
audioGraph.sound.Volume = 0
audioGraph.sound:Play()
end
end)
print(RESPONSE_PREFIX.."Playing set '"..setName.."' (by "..player.Name..")")
elseif cmd == "/play" then
local assetId = tostring(args[1])
pcall(function()
audioGraph.player.AssetId = "rbxassetid://" .. assetId
audioGraph.player.PlaybackSpeed = 1
audioGraph.player.TimePosition = 0
audioGraph.player:Play()
if audioGraph.sound then
audioGraph.sound.SoundId = "rbxassetid://" .. assetId
audioGraph.sound.Volume = 0
audioGraph.sound:Play()
end
end)
print(RESPONSE_PREFIX.."Playing AssetId "..assetId.." (by "..player.Name..")")
elseif cmd == "/stop" then
if audioGraph.player then audioGraph.player:Stop() end
if audioGraph.sound then audioGraph.sound:Stop() end
print(RESPONSE_PREFIX.."Playback stopped by "..player.Name)
elseif cmd == "/volume" then
local volArg = tonumber(args[1])
if not volArg then print(RESPONSE_PREFIX.."Usage: /volume [0-100]"); return end
masterVolume = math.clamp(volArg, 0, 100) / 100
updateAudioGraph()
print(RESPONSE_PREFIX..string.format("Master volume set to %d%% by %s", volArg, player.Name))
elseif cmd == "/preset" then
local presetName = args[1] and args[1]:lower()
local preset = PRESETS[presetName]
if not preset then
local available = {}
for name in pairs(PRESETS) do table.insert(available, name) end
print(RESPONSE_PREFIX.."Invalid preset. Available: "..table.concat(available, ", "))
return
end
-- Apply preset
for key, state in pairs(channelState) do
local cfg = CHANNELS[key]
for fxType, params in pairs(preset) do
if fxType == "eq" and not cfg.isSub then
for k, v in pairs(params) do state.eq[k] = v end
elseif fxType == "subEq" and cfg.isSub then
for k, v in pairs(params) do state.eq[k] = v end
elseif state[fxType] then
for k, v in pairs(params) do state[fxType][k] = v end
end
end
end
updateAudioGraph()
print(RESPONSE_PREFIX..preset.response.." (Set by "..player.Name..")")
end
end
-- | Event Listeners |
local function onPlayerAdded(player)
player.Chatted:Connect(function(msg)
if not ADMIN_USERS[player.Name] then return end
if msg:sub(1,1) ~= "/" then return end
local args = msg:split(" ")
local cmd = table.remove(args,1)
handleCommand(player, cmd, args)
end)
end
-- | Initialization |
setupAudioGraph()
task.wait(0.2)
updateAudioGraph()
Players.PlayerAdded:Connect(onPlayerAdded)
for _, p in ipairs(Players:GetPlayers()) do onPlayerAdded(p) end
-- Sync hidden sound for analysis
RunService.Heartbeat:Connect(function()
if audioGraph.player and audioGraph.sound and audioGraph.player.IsPlaying then
if not audioGraph.sound.IsPlaying then
audioGraph.sound.SoundId = audioGraph.player.AssetId
audioGraph.sound.PlaybackSpeed = audioGraph.player.PlaybackSpeed
audioGraph.sound.Volume = 0
audioGraph.sound:Play()
end
if math.abs(audioGraph.player.TimePosition - audioGraph.sound.TimePosition) > 0.15 then
audioGraph.sound.TimePosition = audioGraph.player.TimePosition
end
end
end)
**(ListenerClient LocalScript)**
-- | Services |
local Workspace = game:GetService("Workspace")
local Players = game:GetService("Players")
local LocalPlayer = Players.LocalPlayer
local function getLayout()
local sonicHavoc = Workspace:FindFirstChild("[Sonic Havoc] Workspace")
if not sonicHavoc then return "Stereo" end
local worker = sonicHavoc:FindFirstChild("Worker")
if not worker then return "Stereo" end
local config = worker:FindFirstChild("Configuration")
if not config then return "Stereo" end
local v = config:FindFirstChild("ChannelLayout")
if v and typeof(v.Value) == "string" then
local s = v.Value:lower()
if s == "quad" or s == "quadraphonic" then return "Quad" end
end
return "Stereo"
end
local CHANNEL_LAYOUT = getLayout()
local camera = Workspace.CurrentCamera
if not camera then
Workspace:GetPropertyChangedSignal("CurrentCamera"):Wait()
camera = Workspace.CurrentCamera
end
local audioContainer = Instance.new("Folder")
audioContainer.Name = "ClientAudioSystem"
audioContainer.Parent = LocalPlayer:WaitForChild("PlayerScripts")
local WireContainer = Instance.new("Folder")
WireContainer.Name = "Wires"
WireContainer.Parent = audioContainer
local function wireUp(source, target, sourceName, targetName)
local wire = Instance.new("Wire")
wire.SourceInstance = source
wire.TargetInstance = target
if sourceName then wire.SourceName = sourceName end
if targetName then wire.TargetName = targetName end
wire.Parent = WireContainer
return wire
end
local listener = Instance.new("AudioListener")
local splitter = Instance.new("AudioChannelSplitter")
local mixer = Instance.new("AudioChannelMixer")
local output = Instance.new("AudioDeviceOutput")
listener.Parent = camera
splitter.Parent = audioContainer
mixer.Parent = audioContainer
output.Parent = audioContainer
pcall(function()
if CHANNEL_LAYOUT == "Quad" then
splitter.Layout = Enum.AudioChannelLayout.Quad
mixer.Layout = Enum.AudioChannelLayout.Quad
else
splitter.Layout = Enum.AudioChannelLayout.Stereo
mixer.Layout = Enum.AudioChannelLayout.Stereo
end
end)
wireUp(listener, splitter)
wireUp(mixer, output)
local leftFader = Instance.new("AudioFader")
leftFader.Name = "LeftChannelFader"
leftFader.Volume = 1.0
leftFader.Parent = audioContainer
local rightFader = Instance.new("AudioFader")
rightFader.Name = "RightChannelFader"
rightFader.Volume = 1.0
rightFader.Parent = audioContainer
print("[Sonic Havoc] Created Left and Right channel faders.")
if mixer.Layout == Enum.AudioChannelLayout.Stereo then
wireUp(splitter, leftFader, "Left")
wireUp(leftFader, mixer, nil, "Left")
wireUp(splitter, rightFader, "Right")
wireUp(rightFader, mixer, nil, "Right")
print("[Sonic Havoc] Wired Stereo signal chain with effects.")
else
wireUp(splitter, mixer, "FrontLeft", "FrontLeft")
wireUp(splitter, mixer, "FrontRight", "FrontRight")
wireUp(splitter, mixer, "BackLeft", "BackLeft")
wireUp(splitter, mixer, "BackRight", "BackRight")
end