AudioService - basic way to manage audio

hi, i tried making something easier to make audio using code and theres type checking too

heres how to use:

Tutorial

AudioEmitter with DiffractionEnabled set to Enabled and parented to workspace.

local audioEmitter = AudioService.newAudioEmitter({
	DiffractionEnabled = Enum.SimulationMode.Enabled
}, workspace)

Fader → Distortion → Echo → AudioEmitter

local fader = audioEmitter:AddEffect("Fader")
local distortion = audioEmitter:AddEffect("Distortion")
local echo = audioEmitter:AddEffect("Echo")

AudioPlayer → Fader → Distortion → Echo → AudioEmitter

local audio = AudioService.newAudioPlayer(1234567890, nil, audioEmitter.instance)

AudioPlayer → Echo → Fader → Distortion → Echo → AudioEmitter

local echo = audio:AddEffect("Echo", nil, audioEmitter)

AudioPlayer → Echo → Reverb → Fader → Distortion → Echo → AudioEmitter
AudioPlayer → Echo → Reverb → AudioEmitter2

local reverb = audio:AddEffect("Reverb", nil, {audioEmitter, audioEmitter2})

AudioPlayer → Reverb → Fader → Distortion → Echo → AudioEmitter
AudioPlayer → Reverb → AudioEmitter2

audio:RemoveEffect(echo)

AudioPlayer → Fader → Distortion → Echo → AudioEmitter
AudioPlayerAudioEmitter2

audio:ClearEffects()

AudioPlayerAudioEmitter
AudioPlayerAudioEmitter2

audioEmitter:ClearEffects()

Inserts an attachment in workspace at the position, copies audioplayer, wires, and audioemitters and puts it inside the attachment and plays the audio

audio:PlayAtPosition(Vector3.new(0, 10, 0))
Code
AudioService
local AudioPlayerController = require("@self/AudioPlayerController")
local AudioEmitterController = require("@self/AudioEmitterController")

return {
	newAudioPlayer = AudioPlayerController.new,
	newAudioEmitter = AudioEmitterController.new,
}
AudioUtils
local AudioTypes = require("./AudioTypes")

local AudioUtils = {}

function AudioUtils.writeProperty(instance: Instance, property: string, value: any)
	instance[property] = value
end

function AudioUtils.writePropertySafe(instance: Instance, property: string, value: any): boolean
	local success, err = pcall(AudioUtils.writeProperty, instance, property, value)

	if not success then
		warn(`Error while setting property {property} on {instance.ClassName}: {err}`)
		return false
	end

	return true
end

function AudioUtils.writeProperties(instance: Instance, properties: {[string]: any})
	for property, value in properties do
		AudioUtils.writePropertySafe(instance, property, value)
	end
end

function AudioUtils.createEffect(effect: string | AudioTypes.Effect, properties: {[string]: any}?, parent: Instance): Instance
	local effectInstance = Instance.new("Audio"..effect)

	if properties then
		AudioUtils.writeProperties(effectInstance, properties)
	end

	effectInstance.Parent = parent
	return effectInstance
end

return AudioUtils
AudioPlayerController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")
local AudioWiresController = require("./AudioWiresController")

local AudioPlayerController = {}
AudioPlayerController.__index = AudioPlayerController

export type AudioPlayerController = AudioTypes.AudioPlayerController
type AudioPlayerControllerType = AudioPlayerController

function AudioPlayerController.new(audioId: number, properties: {[string]: any}?, parent: Instance?): AudioPlayerController
	local audioPlayer = Instance.new("AudioPlayer")
	audioPlayer.Asset = "rbxassetid://"..audioId

	if properties then
		AudioUtils.writeProperties(audioPlayer, properties)
	end

	if parent then
		audioPlayer.Parent = parent
	end
	
	local self = {
		instance = audioPlayer,
		audioEmitterControllers = {},
		effects = {},
	} :: AudioPlayerController
	
	self.audioWiresController = AudioWiresController.new(self)
	setmetatable(self, AudioPlayerController)
	return self :: any
end

function AudioPlayerController.ChangeAudioId(self: AudioPlayerController, audioId: number): AudioPlayerController
	self.instance.Asset = "rbxassetid://"..audioId
	return self
end

function AudioPlayerController.AddEffect(self: AudioPlayerController, effect: string, properties: {[string]: any}?, channel: AudioTypes.Channel?): Instance
	local effectInstance = AudioUtils.createEffect(effect, properties, self.instance)
	self:AddEffectFromInstance(effectInstance, channel)
	return effectInstance
end

function AudioPlayerController.AddEffectFromInstance(self: AudioPlayerController, effectInstance: Instance | {Instance}, channel: AudioTypes.Channel?)
	if type(effectInstance) == "table" then
		table.move(self.effects, 1, #effectInstance, #self.effects + 1, effectInstance)
		
		for _, effect in effectInstance do
			self.audioWiresController:ConnectWireToChannel(effect, channel)
		end
	else
		table.insert(self.effects, effectInstance)
		self.audioWiresController:ConnectWireToChannel(effectInstance, channel)
	end
end

function AudioPlayerController.RemoveEffect(self: AudioPlayerController, effectInstance: Instance | {Instance}, channel: AudioTypes.Channel?): boolean
	if type(effectInstance) == "table" then
		for _, effect in effectInstance do
			self:RemoveEffect(effect, channel)
		end
		return true
	end
	
	local index = table.find(self.effects, effectInstance)

	if not index then
		return false
	end

	table.remove(self.effects, index)
	self.audioWiresController:DisconnectWireFromChannel(effectInstance, channel)
	effectInstance:Destroy()
	return true
end

function AudioPlayerController.ClearEffects(self: AudioPlayerController)
	self.audioWiresController:DisconnectAllWires()
	
	for _, effectInstance in self.effects do
		effectInstance:Destroy()
	end
	
	table.clear(self.effects)
end

function AudioPlayerController.PlayAtPosition(self: AudioPlayerController, position: Vector3 | vector, atTime: number?)
	local attachment = Instance.new("Attachment")
	attachment.WorldPosition = position
	
	local newPlayer = Instance.fromExisting(self.instance)
	
	local newEmitter: AudioEmitter
	local newEffect: Instance
	local mainWire: Wire
	local wire: Wire
	local targetInstance: Instance
	
	for audioEmitterController, chain in self.audioWiresController.channels do
		if #chain == 0 then
			continue
		end
		
		newEmitter = Instance.fromExisting(audioEmitterController.instance)
		
		mainWire = Instance.new("Wire")
		mainWire.SourceInstance = newPlayer
		
		for i, effectInstance in chain do
			newEffect = Instance.fromExisting(effectInstance)
			
			if i == 1 then
				mainWire.TargetInstance = newEffect
			end
			
			if wire then
				wire.TargetInstance = newEffect
				wire.Parent = newEmitter
			end

			wire = Instance.new("Wire")
			wire.SourceInstance = newEffect
			newEffect.Parent = newPlayer
		end
		
		if wire then
			wire.TargetInstance = newEmitter
			wire.Parent = newEmitter
		end
		
		mainWire.Parent = newEmitter
		newEmitter.Parent = attachment
	end

	newPlayer.Parent = attachment
	attachment.Parent = workspace

	local cleaned = false

	local function cleanup()
		if cleaned then
			return
		end

		cleaned = true
		attachment:Destroy()
	end

	newPlayer.Ended:Once(cleanup)
	newPlayer.Destroying:Once(cleanup)
	
	newPlayer:Play(atTime)
end

function AudioPlayerController.Destroy(self: AudioPlayerController)
	self.audioWiresController:Destroy()
	self.instance:Destroy()
	
	for _, effectInstance in self.effects do
		effectInstance:Destroy()
	end
	
	table.clear(self.effects)
	setmetatable(self, nil)
end

return AudioPlayerController :: {
	new: (audioId: number, properties: AudioTypes.GetProperties<AudioPlayer>?, parent: Instance?) -> AudioPlayerController,
}
AudioEmitterController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")
local AudioChannelWiresController = require("./AudioChannelWiresController")

local AudioEmitterController = {}
AudioEmitterController.__index = AudioEmitterController

export type AudioEmitterController = AudioTypes.AudioEmitterController

function AudioEmitterController.new(properties: {[string]: any}?, parent: Instance?): AudioEmitterController
	local audioEmitter = Instance.new("AudioEmitter")

	if properties then
		AudioUtils.writeProperties(audioEmitter, properties)
	end

	if parent then
		audioEmitter.Parent = parent
	end

	local self = {
		instance = audioEmitter,
		audioPlayerControllers = {},
		effects = {},
	} :: AudioEmitterController

	self.audioChannelWiresController = AudioChannelWiresController.new(self)
	setmetatable(self, AudioEmitterController)
	return self :: any
end

function AudioEmitterController.GetInputInstance(self: AudioEmitterController): Instance
	return self.effects[1] or self.instance
end

function AudioEmitterController.AddEffect(self: AudioEmitterController, effect: string, properties: {[string]: any}?): Instance
	local effectInstance = AudioUtils.createEffect(effect, properties, self.instance)
	self:AddEffectFromInstance(effectInstance)
	return effectInstance
end

function AudioEmitterController.AddEffectFromInstance(self: AudioEmitterController, effectInstance: Instance | {Instance})
	if type(effectInstance) == "table" then
		table.move(self.effects, 1, #effectInstance, #self.effects + 1, effectInstance)
		
		for _, effect in effectInstance do
			self.audioChannelWiresController:ConnectWire(effect)
		end
	else
		table.insert(self.effects, effectInstance)
		self.audioChannelWiresController:ConnectWire(effectInstance)
	end
end

function AudioEmitterController.RemoveEffect(self: AudioEmitterController, effectInstance: Instance | {Instance}): boolean
	if type(effectInstance) == "table" then
		for _, effect in effectInstance do
			self:RemoveEffect(effect)
		end
		return true
	end
	
	local index = table.find(self.effects, effectInstance)

	if not index then
		return false
	end

	table.remove(self.effects, index)
	self.audioChannelWiresController:DisconnectWire(effectInstance)
	effectInstance:Destroy()
	return true
end

function AudioEmitterController.ClearEffects(self: AudioEmitterController)
	self.audioChannelWiresController:DisconnectAllWires()
	
	for _, effectInstance in self.effects do
		effectInstance:Destroy()
	end

	table.clear(self.effects)
end

function AudioEmitterController.Destroy(self: AudioEmitterController)
	self.audioChannelWiresController:Destroy()
	self.instance:Destroy()
	
	for _, effectInstance in self.effects do
		effectInstance:Destroy()
	end

	table.clear(self.effects)
	setmetatable(self, nil)
end

return AudioEmitterController :: {
	new: (properties: AudioTypes.GetProperties<AudioEmitter>?, parent: Instance?) -> AudioEmitterController,
}
AudioWiresController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")

local AudioWiresController = {}
AudioWiresController.__index = AudioWiresController

export type AudioWiresController = AudioTypes.AudioWiresController

function AudioWiresController.new(audioPlayerController: AudioTypes.AudioPlayerController): AudioWiresController
	return setmetatable({
		audioPlayerController = audioPlayerController,
		channels = {},
		wiresPool = {},
		activeWires = {},
	}, AudioWiresController) :: any
end

function AudioWiresController._pullWire(self: AudioWiresController, sourceInstance: Instance, targetInstance: Instance): Wire
	local wire = table.remove(self.wiresPool) or Instance.new("Wire")
	wire.SourceInstance = sourceInstance
	wire.TargetInstance = targetInstance
	wire.Parent = self.audioPlayerController.instance
	return wire
end

function AudioWiresController._pushWireToPool(self: AudioWiresController, sourceInstance: Instance, targetInstance: Instance): boolean
	local activeWiresSource = self.activeWires[sourceInstance]
	
	if not activeWiresSource then
		return false
	end
	
	local wire = activeWiresSource[targetInstance]
	
	if not wire then
		return false
	end
	
	activeWiresSource[targetInstance] = nil
	wire.Parent = nil
	wire.SourceInstance = nil
	wire.TargetInstance = nil
	table.insert(self.wiresPool, wire)
	
	if not next(activeWiresSource) then
		self.activeWires[sourceInstance] = nil
	end
	return true
end

function AudioWiresController._connectWireToChannel(self: AudioWiresController, targetInstance: Instance, singleChannel: AudioTypes.SingleChannel)
	local chain = self.channels[singleChannel]

	if not chain then
		chain = {}
		self.channels[singleChannel] = chain
	end

	local activeWiresSource = self.activeWires[singleChannel.instance]
	local previousEffectInstance = chain[#chain]

	if previousEffectInstance then
		local previousWire = activeWiresSource[previousEffectInstance]
		previousWire.TargetInstance = targetInstance
	else
		if not activeWiresSource then
			local wire = self:_pullWire(self.audioPlayerController.instance, targetInstance)
			activeWiresSource = {[self.audioPlayerController.instance :: Instance] = wire}
			self.activeWires[singleChannel.instance] = activeWiresSource
		else
			local wire = activeWiresSource[self.audioPlayerController.instance]
			wire.TargetInstance = targetInstance
		end
	end

	local channelInstance = singleChannel:GetInputInstance()
	local wire = self:_pullWire(targetInstance, channelInstance)

	activeWiresSource[targetInstance] = wire
	table.insert(chain, targetInstance)
	
	self.audioPlayerController.audioEmitterControllers[singleChannel] = true
	singleChannel.audioPlayerControllers[self.audioPlayerController] = true
end

function AudioWiresController.ConnectWireToChannel(self: AudioWiresController, targetInstance: Instance, channel: AudioTypes.Channel?)
	if not channel then
		local copy = {}
		
		for singleChannel in self.channels do
			table.insert(copy, singleChannel)
		end
		
		for _, singleChannel in copy do
			self:_connectWireToChannel(targetInstance, singleChannel)
		end
		
	elseif type(channel) == "table" and not channel.instance then
		for _, singleChannel in channel do
			self:_connectWireToChannel(targetInstance, singleChannel)
		end
	else
		self:_connectWireToChannel(targetInstance, channel)
	end
end

function AudioWiresController._disconnectWireFromChannel(self: AudioWiresController, targetInstance: Instance, singleChannel: AudioTypes.SingleChannel): boolean
	local chain = self.channels[singleChannel]
	
	if not chain then
		return false
	end
	
	local index = table.find(chain, targetInstance)
	
	if not index then
		return false
	end
	
	table.remove(chain, index)
	
	local previousSourceInstance = chain[index - 1] or self.audioPlayerController.instance
	local nextTargetInstance = chain[index] or singleChannel.instance
	
	local activeWiresSource = self.activeWires[singleChannel.instance]
	local previousWire = activeWiresSource[previousSourceInstance]
	previousWire.TargetInstance = nextTargetInstance
	
	self:_pushWireToPool(singleChannel.instance, targetInstance)
	
	if #chain == 0 then
		self.channels[singleChannel] = nil
	end
	
	self.audioPlayerController.audioEmitterControllers[singleChannel] = nil
	singleChannel.audioPlayerControllers[self.audioPlayerController] = nil
	return true
end

function AudioWiresController.DisconnectWireFromChannel(self: AudioWiresController, targetInstance: Instance, channel: AudioTypes.Channel?): boolean
	local removedAny = false
	
	if not channel then
		local copy = {}

		for singleChannel in self.channels do
			table.insert(copy, singleChannel)
		end

		for _, singleChannel in copy do
			local removed = self:_disconnectWireFromChannel(targetInstance, singleChannel)

			if removed then
				removedAny = true
			end
		end

	elseif type(channel) == "table" and not channel.instance then
		for _, singleChannel in channel do
			local removed = self:_disconnectWireFromChannel(targetInstance, singleChannel)
			
			if removed then
				removedAny = true
			end
		end
	else
		removedAny = self:_disconnectWireFromChannel(targetInstance, channel)
	end
	
	return removedAny
end

function AudioWiresController.DisconnectAllWires(self: AudioWiresController)
	for singleChannel, chain in self.channels do
		local activeWiresSource = self.activeWires[singleChannel.instance]

		if not activeWiresSource then
			continue
		end
		
		local wire = activeWiresSource[self.audioPlayerController.instance]
		wire.TargetInstance = singleChannel:GetInputInstance()
		
		for _, targetInstance in chain do
			self:_pushWireToPool(singleChannel.instance, targetInstance)
		end
		
		table.clear(chain)
		
		self.audioPlayerController.audioEmitterControllers[singleChannel] = nil
		singleChannel.audioPlayerControllers[self.audioPlayerController] = nil
	end
end

function AudioWiresController.DestroyAllWires(self: AudioWiresController)
	for singleChannel in self.channels do
		local activeWiresSource = self.activeWires[singleChannel.instance]

		if not activeWiresSource then
			continue
		end

		for _, wire in activeWiresSource do
			wire:Destroy()
		end
		
		self.audioPlayerController.audioEmitterControllers[singleChannel] = nil
		singleChannel.audioPlayerControllers[self.audioPlayerController] = nil
	end

	table.clear(self.channels)
	table.clear(self.activeWires)
	
	for _, wire in self.wiresPool do
		wire:Destroy()
	end
	
	table.clear(self.wiresPool)
end

function AudioWiresController.Destroy(self: AudioWiresController)
	self:DestroyAllWires()
	setmetatable(self, nil)
end

return AudioWiresController :: {
	new: (audioPlayerController: AudioTypes.AudioPlayerController) -> AudioWiresController,
}
AudioChannelWiresController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")

local AudioChannelWiresController = {}
AudioChannelWiresController.__index = AudioChannelWiresController

export type AudioChannelWiresController = AudioTypes.AudioChannelWiresController

function AudioChannelWiresController.new(channel: AudioTypes.SingleChannel): AudioChannelWiresController
	return setmetatable({
		channel = channel,
		wiresPool = {},
		activeWires = {},
		chain = {},
	}, AudioChannelWiresController) :: any
end

function AudioChannelWiresController._pullWire(self: AudioChannelWiresController, sourceInstance: Instance, targetInstance: Instance): Wire
	local wire = table.remove(self.wiresPool) or Instance.new("Wire")
	wire.SourceInstance = sourceInstance
	wire.TargetInstance = targetInstance
	wire.Parent = self.channel.instance
	return wire
end

function AudioChannelWiresController._pushWireToPool(self: AudioChannelWiresController, targetInstance: Instance): boolean
	local wire = self.activeWires[targetInstance]
	
	if not wire then
		return false
	end
	
	self.activeWires[targetInstance] = nil
	wire.Parent = nil
	wire.SourceInstance = nil
	wire.TargetInstance = nil
	table.insert(self.wiresPool, wire)
	return true
end

function AudioChannelWiresController.ConnectWire(self: AudioChannelWiresController, targetInstance: Instance)
	local previousEffectInstance = self.chain[#self.chain]

	if previousEffectInstance then
		local previousWire = self.activeWires[previousEffectInstance]
		previousWire.TargetInstance = targetInstance
	end

	self.activeWires[targetInstance] = self:_pullWire(targetInstance, self.channel.instance)
	table.insert(self.chain, targetInstance)
end

function AudioChannelWiresController.DisconnectWire(self: AudioChannelWiresController, targetInstance: Instance): boolean
	local index = table.find(self.chain, targetInstance)
	
	if not index then
		return false
	end
	
	table.remove(self.chain, index)
	
	local previousEffect = self.chain[index - 1]
	
	if previousEffect then
		local previousWire = self.activeWires[previousEffect]
		local nextTarget = self.chain[index] or self.channel.instance
		previousWire.TargetInstance = nextTarget
	end
	
	if index == 1 then
		self:ReattachWires()
	end
	
	return self:_pushWireToPool(targetInstance)
end

function AudioChannelWiresController.DisconnectAllWires(self: AudioChannelWiresController)
	for _, effectInstance in self.chain do
		self:_pushWireToPool(effectInstance)
	end
	
	table.clear(self.chain)
	self:ReattachWires()
end

function AudioChannelWiresController.DestroyAllWires(self: AudioChannelWiresController)
	for _, effectInstance in self.chain do
		self.activeWires[effectInstance]:Destroy()
	end

	table.clear(self.chain)
	table.clear(self.activeWires)
	
	for _, wire in self.wiresPool do
		wire:Destroy()
	end
	
	table.clear(self.wiresPool)
	self:ReattachWires()
end

function AudioChannelWiresController.ReattachWires(self: AudioChannelWiresController)
	local inputInstance = self.channel:GetInputInstance()

	for audioPlayerController in self.channel.audioPlayerControllers do
		local activeWiresSource = audioPlayerController.audioWiresController.activeWires[self.channel.instance]
		local chain = audioPlayerController.audioWiresController.channels[self.channel]
		local effectInstance = chain[#chain]
		local wire = activeWiresSource[effectInstance]

		wire.TargetInstance = inputInstance
	end
end

function AudioChannelWiresController.Destroy(self: AudioChannelWiresController)
	self:DestroyAllWires()
	setmetatable(self, nil)
end

return AudioChannelWiresController :: {
	new: (channel: AudioTypes.SingleChannel) -> AudioChannelWiresController,
}
AudioTypes
export type AudioPlayerController = setmetatable<{
	instance: AudioPlayer,
	audioEmitterControllers: {[AudioEmitterController]: boolean},
	attachment: Attachment?,
	audioWiresController: AudioWiresController,
	effects: {Instance},

	ChangeAudioId: (self: AudioPlayerController, audioId: number) -> AudioPlayerController,
	AddEffect: <T>(self: AudioPlayerController, effect: T | Effect, properties: GetEffectProperties<T>?, channel: Channel?) -> GetEffectInstance<T>,
	AddEffectFromInstance: (self: AudioPlayerController, effectInstance: Instance | {Instance}, channel: Channel?) -> (),
	RemoveEffect: <T>(self: AudioPlayerController, effectInstance: Instance | {Instance}, channel: Channel?) -> boolean,
	ClearEffects: (self: AudioPlayerController) -> (),
	PlayAtPosition: (self: AudioPlayerController, position: Vector3 | vector, atTime: number?) -> (),
	Destroy: (self: AudioPlayerController) -> ()
}, {
	__index: AudioPlayerController
}>

export type AudioEmitterController = setmetatable<{
	instance: AudioEmitter,
	audioPlayerControllers: {[AudioPlayerController]: boolean},
	audioChannelWiresController: AudioChannelWiresController,
	effects: {Instance},
	
	GetInputInstance: (self: AudioEmitterController) -> Instance,
	AddEffect: <T>(self: AudioEmitterController, effect: T | Effect, properties: GetEffectProperties<T>?) -> GetEffectInstance<T>,
	AddEffectFromInstance: (self: AudioEmitterController, effectInstance: Instance | {Instance}) -> (),
	RemoveEffect: <T>(self: AudioEmitterController, effectInstance: Instance | {Instance}) -> boolean,
	ClearEffects: (self: AudioEmitterController) -> (),
	Destroy: (self: AudioEmitterController) -> (),
}, {
	__index: AudioEmitterController
}>

export type AudioWiresController = setmetatable<{
	audioPlayerController: AudioPlayerController,
	channels: {[AudioEmitterController]: {Instance}},
	wiresPool: {Wire},
	activeWires: {[Instance]: {[Instance]: Wire}},
	
	_pullWire: (self: AudioWiresController, sourceInstance: Instance, targetInstance: Instance) -> Wire,
	_pushWireToPool: (self: AudioWiresController, sourceInstance: Instance, targetInstance: Instance) -> boolean,
	_connectWireToChannel: (self: AudioWiresController, targetInstance: Instance, channel: AudioEmitterController) -> (),
	ConnectWireToChannel: (self: AudioWiresController, targetInstance: Instance, channel: Channel?) -> (),
	_disconnectWireFromChannel: (self: AudioWiresController, targetInstance: Instance, channel: AudioEmitterController) -> boolean,
	DisconnectWireFromChannel: (self: AudioWiresController, targetInstance: Instance, channel: Channel?) -> boolean,
	DisconnectAllWires: (self: AudioWiresController) -> (),
	DestroyAllWires: (self: AudioWiresController) -> (),
	Destroy: (self: AudioWiresController) -> (),
}, {
	__index: AudioWiresController
}>

export type AudioChannelWiresController = setmetatable<{
	channel: SingleChannel,
	wiresPool: {Wire},
	activeWires: {[Instance]: Wire},
	chain: {Instance},
	
	_pullWire: (self: AudioChannelWiresController, sourceInstance: Instance, targetInstance: Instance) -> Wire,
	_pushWireToPool: (self: AudioChannelWiresController, targetInstance: Instance) -> boolean,
	ConnectWire: (self: AudioChannelWiresController, targetInstance: Instance) -> (),
	DisconnectWire: (self: AudioChannelWiresController, targetInstance: Instance) -> boolean,
	DisconnectAllWires: (self: AudioChannelWiresController) -> (),
	DestroyAllWires: (self: AudioChannelWiresController) -> (),
	ReattachWires: (self: AudioChannelWiresController) -> (),
	Destroy: (self: AudioChannelWiresController) -> (),
}, {
	__index: AudioChannelWiresController
}>

export type function GetProperties(T: type)
	local result = types.newtable()
	
	for key, property in T:properties() do
		if not property.write then
			continue
		end
		
		if property.write:is("function") then
			continue
		end
		
		result:setproperty(key, types.optional(property.write))
	end
	
	return result
end

type rawIndex<T, U> = rawget<T, U>

export type function GetEffectProperties(T: type)
	if not T:is("singleton") then
		return types.any
	end
	
	local result = rawIndex(AudioProperties, T)
	return if result ~= types.singleton(nil) then result else types.singleton(nil)
end

export type function GetEffectInstance(T: type)
	if not T:is("singleton") then
		return types.any
	end
	
	local result = rawIndex(AudioInstances, T)
	return if result ~= types.singleton(nil) then result else types.singleton(nil)
end

export type SingleChannel = AudioEmitterController
export type Channel = SingleChannel | {SingleChannel}
export type Effect = keyof<AudioInstances>

export type AudioInstances = {
	Equalizer: AudioReverb,
	Compressor: AudioCompressor,
	Reverb: AudioReverb,
	Chorus: AudioChorus,
	Distortion: AudioDistortion,
	Echo: AudioEcho,
	Flanger: AudioFlanger,
	PitchShifter: AudioPitchShifter,
	Tremolo: AudioTremolo,
	Fader: AudioFader,
}

export type AudioProperties = {
	Equalizer: GetProperties<AudioReverb>,
	Compressor: GetProperties<AudioCompressor>,
	Reverb: GetProperties<AudioReverb>,
	Chorus: GetProperties<AudioChorus>,
	Distortion: GetProperties<AudioDistortion>,
	Echo: GetProperties<AudioEcho>,
	Flanger: GetProperties<AudioFlanger>,
	PitchShifter: GetProperties<AudioPitchShifter>,
	Tremolo: GetProperties<AudioTremolo>,
	Fader: GetProperties<AudioFader>,
}

return true

if there are any bugs, please tell me so that i can fix it. it only just learned about it a few days ago

3 Likes

wanted to post an update because the original version didn’t seem very flexible to me so i changed the way you wire things and other changes

update
AudioService
local AudioPlayerController = require("@self/AudioPlayerController")
local AudioEffectController = require("@self/AudioEffectController")
local AudioEmitterController = require("@self/AudioEmitterController")

return {
	audioPlayer = AudioPlayerController,
	audioEffect = AudioEffectController,
	audioEmitter = AudioEmitterController,
}
AudioUtils
local AudioTypes = require("./AudioTypes")

local AudioUtils = {}

function AudioUtils.writeProperty(instance: Instance, property: string, value: any)
	instance[property] = value
end

function AudioUtils.writePropertySafe(instance: Instance, property: string, value: any): boolean
	local success, err = pcall(AudioUtils.writeProperty, instance, property, value)

	if not success then
		warn(`Error while setting property {property} on {instance.ClassName}: {err}`)
		return false
	end

	return true
end

function AudioUtils.writeProperties(instance: Instance, properties: {[string]: any})
	for property, value in properties do
		AudioUtils.writePropertySafe(instance, property, value)
	end
end

return AudioUtils
AudioTypes
export type BaseController<T=Instance, U=Tails?, V=Heads?> = {
	instance: T,
	audioWiresController: AudioWiresController,
	tails: U,
	heads: V,
}

export type BaseControllerUnion = AudioPlayerController | AudioEffectController | AudioEmitterController

type BaseAudioController = {
	Play: (self: BaseAudioController, atTime: number?) -> number?,
	Stop: (self: BaseAudioController, atTime: number?) -> number?,
	Cancel: (self: BaseAudioController, actionId: number?) -> boolean,
	ConnectTo: (self: BaseAudioController, target: BaseControllerUnion | {BaseControllerUnion}) -> (),
	DisconnectFrom: (self: BaseAudioController, target: BaseControllerUnion | {BaseControllerUnion}) -> boolean,
	DisconnectAll: (self: BaseAudioController) -> (),
	Destroy: (self: BaseAudioController) -> ()
}

export type AudioPlayerController = setmetatable<BaseController<AudioPlayer, nil, Heads> & BaseAudioController & {
	audioEmitterControllers: {[AudioEmitterController]: boolean},

	ChangeAudioId: (self: AudioPlayerController, audioId: number) -> AudioPlayerController,
	PlayAtPosition: (self: AudioPlayerController, position: Vector3 | vector, atTime: number?) -> AudioPlayer,
}, {
	__index: AudioPlayerController
}>

export type AudioEffectController<T=Instance> = setmetatable<BaseController<T, Tails, Heads> & BaseAudioController, {
	__index: AudioEffectController<T>
}>

export type AudioEmitterController = setmetatable<BaseController<AudioEmitter, Tails> & {
	audioPlayerControllers: {[AudioPlayerController]: boolean},

	Destroy: (self: AudioEmitterController) -> (),
}, {
	__index: AudioEmitterController
}>

export type AudioWiresController = setmetatable<{
	baseController: BaseController,
	activeWires: {[BaseController]: Wire},
	tails: Tails?,
	heads: Heads?,
	
	ConnectWire: (self: AudioWiresController, target: BaseController) -> (),
	DisconnectWire: (self: AudioWiresController, target: BaseController) -> boolean,
	DisconnectAllWires: (self: AudioWiresController) -> (),
	Destroy: (self: AudioWiresController) -> (),
}, {
	__index: AudioWiresController
}>

export type Tails = {[BaseController]: boolean}
export type Heads = {[BaseController]: boolean}

export type function GetProperties(T: type)
	local result = types.newtable()
	
	for key, property in T:properties() do
		if not property.write then
			continue
		end
		
		if property.write:is("function") then
			continue
		end
		
		result:setproperty(key, types.optional(property.write))
	end
	
	return result
end

type rawIndex<T, U> = rawget<T, U>

export type function GetEffectProperties(T: type)
	if not T:is("singleton") then
		return types.any
	end
	
	local result = rawIndex(AudioProperties, T)
	return if result ~= types.singleton(nil) then result else types.singleton(nil)
end

export type function GetEffectInstance(T: type)
	if not T:is("singleton") then
		return types.any
	end
	
	local result = rawIndex(AudioInstances, T)
	return if result ~= types.singleton(nil) then result else types.singleton(nil)
end

export type Effect = keyof<AudioInstances>

export type AudioInstances = {
	Equalizer: AudioReverb,
	Compressor: AudioCompressor,
	Reverb: AudioReverb,
	Chorus: AudioChorus,
	Distortion: AudioDistortion,
	Echo: AudioEcho,
	Flanger: AudioFlanger,
	PitchShifter: AudioPitchShifter,
	Tremolo: AudioTremolo,
	Fader: AudioFader,
}

export type AudioProperties = {
	Equalizer: GetProperties<AudioReverb>,
	Compressor: GetProperties<AudioCompressor>,
	Reverb: GetProperties<AudioReverb>,
	Chorus: GetProperties<AudioChorus>,
	Distortion: GetProperties<AudioDistortion>,
	Echo: GetProperties<AudioEcho>,
	Flanger: GetProperties<AudioFlanger>,
	PitchShifter: GetProperties<AudioPitchShifter>,
	Tremolo: GetProperties<AudioTremolo>,
	Fader: GetProperties<AudioFader>,
}

return true
AudioPlayerController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")
local AudioWiresController = require("./AudioWiresController")

local AudioPlayerController = {}
AudioPlayerController.__index = AudioPlayerController

export type AudioPlayerController = AudioTypes.AudioPlayerController
type AudioPlayerControllerType = AudioPlayerController

function AudioPlayerController.new(audioId: number, properties: {[string]: any}?, parent: Instance?): AudioPlayerController
	local audioPlayer = Instance.new("AudioPlayer")
	audioPlayer.Asset = "rbxassetid://"..audioId

	if properties then
		AudioUtils.writeProperties(audioPlayer, properties)
	end

	if parent then
		audioPlayer.Parent = parent
	end
	
	local self = {
		instance = audioPlayer,
		audioEmitterControllers = {},
		effects = {},
		heads = {},
	} :: AudioPlayerController
	
	self.audioWiresController = AudioWiresController.new(self, nil, self.heads)
	return setmetatable(self, AudioPlayerController) :: any
end

function AudioPlayerController.fronExisting(audioPlayer: AudioPlayer): AudioPlayerController
	local self = {
		instance = audioPlayer,
		audioEmitterControllers = {},
		effects = {},
	} :: AudioPlayerController

	self.audioWiresController = AudioWiresController.new(self, nil, {})
	return setmetatable(self, AudioPlayerController) :: any
end

function AudioPlayerController.ChangeAudioId(self: AudioPlayerController, audioId: number): AudioPlayerController
	self.instance.Asset = "rbxassetid://"..audioId
	return self
end

function AudioPlayerController.Play(self: AudioPlayerController, atTime: number?): number?
	return self.instance:Play(atTime)
end

function AudioPlayerController.Stop(self: AudioPlayerController, atTime: number?): number?
	return self.instance:Stop(atTime)
end

function AudioPlayerController.Cancel(self: AudioPlayerController, actionId: number?): boolean
	return self.instance:Cancel(actionId)
end

local function recursiveCopy(sourceInstance: Instance, heads: AudioTypes.Heads, fallbackParent: Instance?)
	for target in heads do
		local targetHeads = target.heads
		local newInstance = Instance.fromExisting(target.instance)

		local wire = Instance.new("Wire")
		wire.SourceInstance = sourceInstance
		wire.TargetInstance = newInstance
		wire.Parent = sourceInstance

		newInstance.Parent = targetHeads and sourceInstance or (fallbackParent or sourceInstance)

		if targetHeads then
			recursiveCopy(newInstance, targetHeads, fallbackParent)
		end
	end
end

function AudioPlayerController.PlayAtPosition(self: AudioPlayerController, position: Vector3 | vector, atTime: number?): AudioPlayer
	local attachment = Instance.new("Attachment")
	attachment.WorldPosition = position

	local newPlayer = Instance.fromExisting(self.instance)
	recursiveCopy(newPlayer, self.heads, attachment)

	newPlayer.Parent = attachment
	attachment.Parent = workspace

	local cleaned = false

	local function cleanup()
		if cleaned then
			return
		end

		cleaned = true
		attachment:Destroy()
	end

	newPlayer.Ended:Once(cleanup)
	newPlayer.Destroying:Once(cleanup)

	newPlayer:Play(atTime)
	return newPlayer
end

function AudioPlayerController.ConnectTo(self: AudioPlayerController, target: AudioTypes.BaseController | {AudioTypes.BaseController})
	if type(target) == "table" and not target.instance then
		for _, target in target do
			self.audioWiresController:ConnectWire(target)
		end
	else
		self.audioWiresController:ConnectWire(target)
	end
end

function AudioPlayerController.DisconnectFrom(self: AudioPlayerController, target: AudioTypes.BaseController | {AudioTypes.BaseController}): boolean
	if type(target) == "table" and not target.instance then
		local removedAny = false
		
		for _, target in target do
			local removed = self.audioWiresController:DisconnectWire(target)
			
			if removed then
				removedAny = true
			end
		end
		
		return removedAny
	end
	
	return self.audioWiresController:DisconnectWire(target)
end

function AudioPlayerController.Strip(self: AudioPlayerController)
	self.audioWiresController:DisconnectAllWires()
end

function AudioPlayerController.Destroy(self: AudioPlayerController)
	self.audioWiresController:Destroy()
	self.instance:Destroy()
	setmetatable(self, nil)
end

return AudioPlayerController :: {
	new: (audioId: number, properties: (AudioTypes.GetProperties<AudioPlayer> & AudioTypes.GetProperties<Instance>)?, parent: Instance?) -> AudioPlayerController,
	fronExisting: (audioPlayer: AudioPlayer) -> AudioPlayerController,
}
AudioEffectController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")
local AudioWiresController = require("./AudioWiresController")

local AudioEffectController = {}
AudioEffectController.__index = AudioEffectController

export type AudioEffectController<T=Instance> = AudioTypes.AudioEffectController<T>

function AudioEffectController.new(effect: string, properties: {[string]: any}?, parent: Instance?): AudioEffectController
	local effectInstance = Instance.new("Audio"..effect)

	if properties then
		AudioUtils.writeProperties(effectInstance, properties)
	end

	effectInstance.Parent = parent

	local self = {
		instance = effectInstance,
		tails = {},
		heads = {},
	} :: AudioEffectController

	self.audioWiresController = AudioWiresController.new(self, self.tails, self.heads)
	return setmetatable(self, AudioEffectController) :: any
end

function AudioEffectController.fromExisting(effectInstance: Instance): AudioEffectController
	local self = {
		instance = effectInstance,
	} :: AudioEffectController

	self.audioWiresController = AudioWiresController.new(self, {}, {})
	return setmetatable(self, AudioEffectController) :: any
end

function AudioEffectController.ConnectTo(self: AudioEffectController, target: AudioTypes.BaseController | {AudioTypes.BaseController})
	if type(target) == "table" and not target.instance then
		for _, target in target do
			self.audioWiresController:ConnectWire(target)
		end
	else
		self.audioWiresController:ConnectWire(target)
	end
end

function AudioEffectController.DisconnectFrom(self: AudioEffectController, target: AudioTypes.BaseController | {AudioTypes.BaseController}): boolean
	if type(target) == "table" and not target.instance then
		local removedAny = false

		for _, target in target do
			local removed = self.audioWiresController:DisconnectWire(target)

			if removed then
				removedAny = true
			end
		end

		return removedAny
	end

	return self.audioWiresController:DisconnectWire(target)
end

function AudioEffectController.Strip(self: AudioEffectController)
	self.audioWiresController:DisconnectAllWires()
end

function AudioEffectController.Destroy(self: AudioEffectController)
	self.audioWiresController:Destroy()
	self.instance:Destroy()
	setmetatable(self, nil)
end

return AudioEffectController :: {
	new: <T>(effect: T | AudioTypes.Effect, properties: (AudioTypes.GetEffectProperties<T> & AudioTypes.GetProperties<Instance>)?, parent: Instance?) -> AudioEffectController<T>,
	fronExisting: <T>(effectInstance: T) -> AudioEffectController<T>,
}
AudioEmitterController
local AudioTypes = require("./AudioTypes")
local AudioUtils = require("./AudioUtils")
local AudioWiresController = require("./AudioWiresController")

local AudioEmitterController = {}
AudioEmitterController.__index = AudioEmitterController

export type AudioEmitterController = AudioTypes.AudioEmitterController

function AudioEmitterController.new(properties: {[string]: any}?, parent: Instance?): AudioEmitterController
	local audioEmitter = Instance.new("AudioEmitter")

	if properties then
		AudioUtils.writeProperties(audioEmitter, properties)
	end

	if parent then
		audioEmitter.Parent = parent
	end

	local self = {
		instance = audioEmitter,
		audioPlayerControllers = {},
		tails = {},
	} :: AudioEmitterController

	self.audioWiresController = AudioWiresController.new(self, self.tails)
	return setmetatable(self, AudioEmitterController) :: any
end

function AudioEmitterController.fromExisting(audioEmitter: AudioEmitter): AudioEmitterController
	local self = {
		instance = audioEmitter,
		audioPlayerControllers = {},
	} :: AudioEmitterController

	self.audioWiresController = AudioWiresController.new(self, {})
	return setmetatable(self, AudioEmitterController) :: any
end

function AudioEmitterController.Destroy(self: AudioEmitterController)
	self.instance:Destroy()
	setmetatable(self, nil)
end

return AudioEmitterController :: {
	new: (properties: (AudioTypes.GetProperties<AudioEmitter> & AudioTypes.GetProperties<Instance>)?, parent: Instance?) -> AudioEmitterController,
	fromExisting: (audioEmitter: AudioEmitter) -> AudioEmitterController,
}
AudioWiresController
local AudioTypes = require("./AudioTypes")
local WirePool = require("./WirePool")

local AudioWiresController = {}
AudioWiresController.__index = AudioWiresController

export type AudioWiresController = AudioTypes.AudioWiresController

function AudioWiresController.new(baseController: AudioTypes.BaseController, tails: AudioTypes.Tails?, heads: AudioTypes.Heads?): AudioWiresController
	return setmetatable({
		baseController = baseController,
		activeWires = {},
		tails = tails,
		heads = heads,
	}, AudioWiresController) :: any
end

function AudioWiresController.ConnectWire(self: AudioWiresController, target: AudioTypes.BaseController)
	if self.activeWires[target] then
		return
	end
	
	self.activeWires[target] = WirePool.pull(self.baseController.instance, target.instance)
	
	local heads = self.heads
	local targetTails = target.audioWiresController.tails

	if heads then
		heads[target] = true
	end
	
	if targetTails then
		targetTails[self.baseController] = true
	end
end

function AudioWiresController.DisconnectWire(self: AudioWiresController, target: AudioTypes.BaseController): boolean
	local wire = self.activeWires[target]
	
	if not wire then
		return false
	end
	
	self.activeWires[target] = nil
	WirePool.push(wire)
	
	local heads = self.heads

	if heads then
		heads[target] = nil
	end
	
	local targetTails = target.audioWiresController.tails

	if targetTails then
		targetTails[self.baseController] = nil
	end
	
	return true
end

function AudioWiresController.DisconnectAllWires(self: AudioWiresController)
	local tails = self.tails
	
	if tails then
		for target in tails do
			target.audioWiresController:DisconnectWire(self.baseController)
		end
		
		table.clear(tails)
	end

	for target, wire in self.activeWires do
		local targetTails = target.audioWiresController.tails
		
		if targetTails then
			targetTails[self.baseController] = nil
		end
		
		WirePool.push(wire)
	end
	
	table.clear(self.activeWires)
end

function AudioWiresController.Destroy(self: AudioWiresController)
	self:DisconnectAllWires()
	setmetatable(self, nil)
end

return AudioWiresController :: {
	new: (baseController: AudioTypes.BaseControllerUnion, tails: AudioTypes.Tails?, heads: AudioTypes.Heads?) -> AudioWiresController,
}
tutorial

creating the emitter which is an instance that the sound comes out of

-- creates an AudioEmitter with the name "Emitter" and other properties 
-- and parented in attachment1
local audioEmitter = AudioService.audioEmitter.new({
	Name = "Emitter",
	AcousticSimulationEnabled = true
}, attachment1)

creating the audio player which plays audio and transfers them through wires and into the emitter

-- creates an AudioPlayer with AssetId, properties, and parented to workspace
local audio = AudioService.audioPlayer.new(9113564970, {Volume = 1.5}, workspace)

creating audio effects that change how the audio sounds

-- auto-complete will show the effect names

-- audio players, audio effects, and audio emitters all have an "instance" property
-- which is the real instance that you gave
local chorus = AudioService.audioEffect.new("Chorus", {Depth = .5}, audio.instance)
local distortion = AudioService.audioEffect.new("Distortion", {Name = "WeirdEffect"}, audio.instance)

connecting them so that it works

audio:ConnectTo(chorus)
chorus:ConnectTo(distortion)
distortion:ConnectTo({audioEmitter1, audioEmitter2}) -- can provide either a table or single
-- this connects the distortion effect to 2 emitters which means
-- the audio plays from 2 emitters

– playing the audio

audio:Play()
audio:PlayAtPosition(Vector3.new(0, 10, 0)) -- or at position
some functions
-- you can also put a roblox instance if you don't want to create it in code
AudioService.audioPlayer.fromExisting(instance)
AudioService.audioEffect.fromExisting(instance)
AudioService.audioEmitter.fromExisting(instance)

audio:ChangeAudioId(123) -- changes asset id without doing a string
audio:DisconnectFrom(effect) -- unplugs the wire from the effect and stores the wire in a pool
audio:DisconnectFrom({effect, effect2, effect3}) -- both ConnectTo and DisconnectFrom accepts a table
audio:Destroy(effect) -- destroys the instance and disconnects all wires that the audio is connected to (doesn't interfere with anything)
how to make audio play from multiple emitters
local attachment1 = Instance.new("Attachment")
attachment1.Name = "Attachment1"
attachment1.WorldPosition = Vector3.new(20, 10, 0)
attachment1.Parent = workspace

local attachment2 = Instance.new("Attachment")
attachment2.Name = "Attachment2"
attachment2.WorldPosition = Vector3.new(-20, 10, 0)
attachment2.Parent = workspace

local audioEmitter1 = AudioService.audioEmitter.new(nil, attachment1)
audioEmitter1.instance.Name = "AudioEmitter1"

local audioEmitter2 = AudioService.audioEmitter.new(nil, attachment2)
audioEmitter2.instance.Name = "AudioEmitter2"

local audio = AudioService.audioPlayer.new(9113564970, nil, workspace)

local chorus = AudioService.audioEffect.new("Chorus", nil, audio.instance)
local distortion = AudioService.audioEffect.new("Distortion", nil, audio.instance)

audio:ConnectTo(chorus)
chorus:ConnectTo(distortion)
distortion:ConnectTo({audioEmitter1, audioEmitter2})

while task.wait(1) do
	audio:Play()
end
2 Likes