Audio "Wireable" classes really need to be using superclasses for their redundant API members

There has been a really clunky API introduction pattern with this recent wave of Audio classes added to Roblox’s engine. The problem became more blatant with this week’s API changes:

This is the current state of Roblox’s Audio API:

Class AudioAnalyzer : Instance
	Property AudioAnalyzer.PeakLevel: number [ReadOnly]
	Property AudioAnalyzer.RmsLevel: number [ReadOnly]
	Property AudioAnalyzer.SpectrumEnabled: boolean
	Property AudioAnalyzer.WindowSize: Enum.AudioWindowSize
	Function AudioAnalyzer:GetConnectedWires(pin: string) -> { Instance }
	Function AudioAnalyzer:GetInputPins() -> { any }
	Function AudioAnalyzer:GetOutputPins() -> { any }
	Function AudioAnalyzer:GetSpectrum() -> { any } [CustomLuaState]
	Event AudioAnalyzer.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioChannelMixer : Instance [NotBrowsable]
	Property AudioChannelMixer.Layout: Enum.AudioChannelLayout
	Function AudioChannelMixer:GetConnectedWires(pin: string) -> { Instance }
	Event AudioChannelMixer.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioChannelSplitter : Instance [NotBrowsable]
	Property AudioChannelSplitter.Layout: Enum.AudioChannelLayout
	Function AudioChannelSplitter:GetConnectedWires(pin: string) -> { Instance }
	Event AudioChannelSplitter.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioChorus : Instance
	Property AudioChorus.Bypass: boolean
	Property AudioChorus.Depth: number
	Property AudioChorus.Mix: number
	Property AudioChorus.Rate: number
	Function AudioChorus:GetConnectedWires(pin: string) -> { Instance }
	Function AudioChorus:GetInputPins() -> { any }
	Function AudioChorus:GetOutputPins() -> { any }
	Event AudioChorus.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioCompressor : Instance
	Property AudioCompressor.Attack: number
	Property AudioCompressor.Bypass: boolean
	Property AudioCompressor.MakeupGain: number
	Property AudioCompressor.Ratio: number
	Property AudioCompressor.Release: number
	Property AudioCompressor.Threshold: number
	Property AudioCompressor.Editor: boolean {RobloxScriptSecurity} [📁 LoadOnly] [NotReplicated]
	Function AudioCompressor:GetConnectedWires(pin: string) -> { Instance }
	Function AudioCompressor:GetInputPins() -> { any }
	Function AudioCompressor:GetOutputPins() -> { any }
	Event AudioCompressor.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioDeviceInput : Instance
	Property AudioDeviceInput.AccessList: BinaryString {RobloxSecurity} [Hidden] [NotScriptable]
	Property AudioDeviceInput.AccessType: Enum.AccessModifierType
	Property AudioDeviceInput.Active: boolean {✏️RobloxScriptSecurity}
	Property AudioDeviceInput.IsReady: boolean {RobloxScriptSecurity} [ReadOnly]
	Property AudioDeviceInput.Muted: boolean
	Property AudioDeviceInput.MutedByLocalUser: boolean {RobloxScriptSecurity} [NotReplicated]
	Property AudioDeviceInput.Player: Player?
	Property AudioDeviceInput.Volume: number
	Function AudioDeviceInput:GetConnectedWires(pin: string) -> { Instance }
	Function AudioDeviceInput:GetInputPins() -> { any }
	Function AudioDeviceInput:GetOutputPins() -> { any }
	Function AudioDeviceInput:GetUserIdAccessList() -> { any }
	Function AudioDeviceInput:SetUserIdAccessList(userIds: { any }) -> ()
	Event AudioDeviceInput.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioDeviceOutput : Instance
	Property AudioDeviceOutput.Player: Player?
	Function AudioDeviceOutput:GetConnectedWires(pin: string) -> { Instance }
	Function AudioDeviceOutput:GetInputPins() -> { any }
	Function AudioDeviceOutput:GetOutputPins() -> { any }
	Event AudioDeviceOutput.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioDistortion : Instance
	Property AudioDistortion.Bypass: boolean
	Property AudioDistortion.Level: number
	Function AudioDistortion:GetConnectedWires(pin: string) -> { Instance }
	Function AudioDistortion:GetInputPins() -> { any }
	Function AudioDistortion:GetOutputPins() -> { any }
	Event AudioDistortion.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioEcho : Instance
	Property AudioEcho.Bypass: boolean
	Property AudioEcho.DelayTime: number
	Property AudioEcho.DryLevel: number
	Property AudioEcho.Feedback: number
	Property AudioEcho.RampTime: number
	Property AudioEcho.WetLevel: number
	Function AudioEcho:GetConnectedWires(pin: string) -> { Instance }
	Function AudioEcho:GetInputPins() -> { any }
	Function AudioEcho:GetOutputPins() -> { any }
	Function AudioEcho:Reset() -> () {RobloxScriptSecurity}
	Event AudioEcho.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioEmitter : Instance
	Property AudioEmitter.AngleAttenuation: BinaryString {RobloxSecurity}
	Property AudioEmitter.DistanceAttenuation: BinaryString {RobloxSecurity}
	Property AudioEmitter.AudioInteractionGroup: string
	Property AudioEmitter.SimulationFidelity: Enum.AudioSimulationFidelity
	Function AudioEmitter:GetAngleAttenuation() -> { [string]: any } [CustomLuaState]
	Function AudioEmitter:GetAudibilityFor(listener: AudioListener) -> number
	Function AudioEmitter:GetConnectedWires(pin: string) -> { Instance }
	Function AudioEmitter:GetDistanceAttenuation() -> { [string]: any } [CustomLuaState]
	Function AudioEmitter:GetInputPins() -> { any }
	Function AudioEmitter:GetInteractingListeners() -> { Instance }
	Function AudioEmitter:GetOutputPins() -> { any }
	Function AudioEmitter:SetAngleAttenuation(curve: { [string]: any }) -> () [CustomLuaState]
	Function AudioEmitter:SetDistanceAttenuation(curve: { [string]: any }) -> () [CustomLuaState]
	Event AudioEmitter.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioEqualizer : Instance
	Property AudioEqualizer.Bypass: boolean
	Property AudioEqualizer.HighGain: number
	Property AudioEqualizer.LowGain: number
	Property AudioEqualizer.MidGain: number
	Property AudioEqualizer.MidRange: NumberRange
	Property AudioEqualizer.Editor: boolean {RobloxScriptSecurity} [📁 LoadOnly] [NotReplicated]
	Function AudioEqualizer:GetConnectedWires(pin: string) -> { Instance }
	Function AudioEqualizer:GetInputPins() -> { any }
	Function AudioEqualizer:GetOutputPins() -> { any }
	Event AudioEqualizer.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioFader : Instance
	Property AudioFader.Bypass: boolean
	Property AudioFader.Volume: number
	Function AudioFader:GetConnectedWires(pin: string) -> { Instance }
	Function AudioFader:GetInputPins() -> { any }
	Function AudioFader:GetOutputPins() -> { any }
	Event AudioFader.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioFilter : Instance
	Property AudioFilter.Bypass: boolean
	Property AudioFilter.FilterType: Enum.AudioFilterType
	Property AudioFilter.Frequency: number
	Property AudioFilter.Gain: number
	Property AudioFilter.Q: number
	Property AudioFilter.Editor: boolean {RobloxScriptSecurity} [📁 LoadOnly] [NotReplicated]
	Function AudioFilter:GetConnectedWires(pin: string) -> { Instance }
	Function AudioFilter:GetGainAt(frequency: number) -> number
	Function AudioFilter:GetInputPins() -> { any }
	Function AudioFilter:GetOutputPins() -> { any }
	Event AudioFilter.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioFlanger : Instance
	Property AudioFlanger.Bypass: boolean
	Property AudioFlanger.Depth: number
	Property AudioFlanger.Mix: number
	Property AudioFlanger.Rate: number
	Function AudioFlanger:GetConnectedWires(pin: string) -> { Instance }
	Function AudioFlanger:GetInputPins() -> { any }
	Function AudioFlanger:GetOutputPins() -> { any }
	Event AudioFlanger.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioLimiter : Instance
	Property AudioLimiter.Bypass: boolean
	Property AudioLimiter.MaxLevel: number
	Property AudioLimiter.Release: number
	Property AudioLimiter.Editor: boolean {RobloxScriptSecurity} [📁 LoadOnly] [NotReplicated]
	Function AudioLimiter:GetConnectedWires(pin: string) -> { Instance }
	Function AudioLimiter:GetInputPins() -> { any }
	Function AudioLimiter:GetOutputPins() -> { any }
	Event AudioLimiter.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioListener : Instance
	Property AudioListener.AudioInteractionGroup: string
	Property AudioListener.AngleAttenuation: BinaryString {RobloxSecurity}
	Property AudioListener.DistanceAttenuation: BinaryString {RobloxSecurity}
	Property AudioListener.SimulationFidelity: Enum.AudioSimulationFidelity
	Function AudioListener:GetAngleAttenuation() -> { [string]: any } [CustomLuaState]
	Function AudioListener:GetAudibilityFor(emitter: AudioEmitter) -> number
	Function AudioListener:GetConnectedWires(pin: string) -> { Instance }
	Function AudioListener:GetDistanceAttenuation() -> { [string]: any } [CustomLuaState]
	Function AudioListener:GetInputPins() -> { any }
	Function AudioListener:GetInteractingEmitters() -> { Instance }
	Function AudioListener:GetOutputPins() -> { any }
	Function AudioListener:Reset() -> () {RobloxScriptSecurity}
	Function AudioListener:SetAngleAttenuation(curve: { [string]: any }) -> () [CustomLuaState]
	Function AudioListener:SetDistanceAttenuation(curve: { [string]: any }) -> () [CustomLuaState]
	Event AudioListener.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioPitchShifter : Instance
	Property AudioPitchShifter.Bypass: boolean
	Property AudioPitchShifter.Pitch: number
	Property AudioPitchShifter.WindowSize: Enum.AudioWindowSize
	Function AudioPitchShifter:GetConnectedWires(pin: string) -> { Instance }
	Function AudioPitchShifter:GetInputPins() -> { any }
	Function AudioPitchShifter:GetOutputPins() -> { any }
	Event AudioPitchShifter.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioPlayer : Instance
	Property AudioPlayer.Asset: string
	Property AudioPlayer.AssetId: string [📁 LoadOnly] [Hidden] [NotReplicated] [Deprecated]
	Property AudioPlayer.AutoLoad: boolean
	Property AudioPlayer.IsReady: boolean [ReadOnly]
	Property AudioPlayer.TimeLength: number [ReadOnly]
	Property AudioPlayer.IsPlaying: boolean {✏️RobloxSecurity}
	Property AudioPlayer.Looping: boolean
	Property AudioPlayer.PlaybackSpeed: number
	Property AudioPlayer.TimePosition: number
	Property AudioPlayer.LoopRegion: NumberRange
	Property AudioPlayer.PlaybackRegion: NumberRange
	Property AudioPlayer.Volume: number
	Function AudioPlayer:GetConnectedWires(pin: string) -> { Instance }
	Function AudioPlayer:GetInputPins() -> { any }
	Function AudioPlayer:GetOutputPins() -> { any }
	Function AudioPlayer:GetWaveformAsync(timeRange: NumberRange, samples: number) -> { any } [Yields]
	Function AudioPlayer:Play() -> ()
	Function AudioPlayer:Stop() -> ()
	Event AudioPlayer.Ended()
	Event AudioPlayer.Looped()
	Event AudioPlayer.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)
Class AudioReverb : Instance
	Property AudioReverb.Bypass: boolean
	Property AudioReverb.DecayRatio: number
	Property AudioReverb.DecayTime: number
	Property AudioReverb.Density: number
	Property AudioReverb.Diffusion: number
	Property AudioReverb.DryLevel: number
	Property AudioReverb.EarlyDelayTime: number
	Property AudioReverb.HighCutFrequency: number
	Property AudioReverb.LateDelayTime: number
	Property AudioReverb.LowShelfFrequency: number
	Property AudioReverb.LowShelfGain: number
	Property AudioReverb.ReferenceFrequency: number
	Property AudioReverb.WetLevel: number
	Function AudioReverb:GetConnectedWires(pin: string) -> { Instance }
	Function AudioReverb:GetInputPins() -> { any }
	Function AudioReverb:GetOutputPins() -> { any }
	Function AudioReverb:Reset() -> () {RobloxScriptSecurity}
	Event AudioReverb.WiringChanged(connected: boolean, pin: string, wire: Wire, instance: Instance)

The Audio instances have no superclass, they all derive from the Instance class. This makes polymorphic analysis of an audio’s connection tree within the confines of Roblox’s typechecking system really frustrating.

This is some code I recently had to write to accomplish that:

local audioClasses = {
    AudioChorus = true,
    AudioCompressor = true,
    AudioDistortion = true,
    AudioEcho = true,
    AudioEqualizer = true,
    AudioFader = true,
    AudioFilter = true,
    AudioFlanger = true,
    AudioLimiter = true,
    AudioPitchShifter = true,
    AudioReverb = true,
}

local inputClasses = {
    AudioEmitter = true,
    AudioAnalyzer = true,
    AudioDeviceOutput = true
}

local outputClasses = {
    AudioPlayer = true,
    AudioDeviceInput = true,
}

local function expandAudioWiring(inst: Instance, callback: (instance: Instance, depth: number) -> (), depth: number?, visited: { [Instance]: true }?)
    local depth = depth or 0
    local visited: typeof(assert(visited)) = visited or {}

    if visited[inst] then
        return
    end

    callback(inst, depth)
    visited[inst] = true

    local input = false
    local output = false
    
    if audioClasses[inst.ClassName] then
        input = true
        output = true
    elseif inputClasses[inst.ClassName] then
        input = true
    elseif outputClasses[inst.ClassName] then
        output = true
    end

    if input then
        local inputs = (inst :: any):GetConnectedWires("Input")

        for _, wire in ipairs(inputs) do
            if wire.SourceInstance then
                expandAudioWiring(wire.SourceInstance, callback, depth + 1, visited)
            end

            if wire.TargetInstance then
                expandAudioWiring(wire.TargetInstance, callback, depth + 1, visited)
            end
        end
    end

    if output then
        local outputs = (inst :: any):GetConnectedWires("Output")

        for _, wire in ipairs(outputs) do
            if wire.SourceInstance then
                expandAudioWiring(wire.SourceInstance, callback, depth + 1, visited)
            end

            if wire.TargetInstance then
                expandAudioWiring(wire.TargetInstance, callback, depth + 1, visited)
            end
        end
    end
end

Notice how I have to manually store a table of ClassNames and use the “:: any” cast to deal with Luau’s type annotations not knowing what the heck is going on here.

This is why I feel like the API design is faulty as it currently stands. It needs refinement to work better in Roblox’s typechecked ecosystem. I provided feedback on this to the engineer working on Roblox’s audio feature previously, but the response I got was rather vague, alluding to some grand vision with wireable instances.

Is there some inheritance pattern here going on that makes it impossible for these members to be unioned together? (i.e. similar to TextLabel/TextBox/TextButton or ImageLabel/ImageButton)

If so then I’ll take it at face value. I just hope something can be done at the reflection level to help clean up this redundancy.

10 Likes

Hey @Maximum_ADHD – internally, we do have an interface/abstract-base-class for these things (Wirable), but currently, stuff that’s exposed to the API only supports single-inheritance.

We are not confident that Wirable is the (one and only) identity that these classes will ever want or need – for example, AudioPlayer is

  • Wirable
  • Playable
  • Seekable
  • Loopable

This problem compounds if we start making other classes wirable; especially if they already inherit from something else

We’re looking at solving this by making “Interfaces” more of a first-class concept in Reflection, but atm there aren’t any firm updates I can provide – so for the time being this is messy :frowning:

In my own code, I’ve been writing something along the lines of

type Wirable = AudioEmitter | AudioPlayer | AudioDeviceOutput 
	| AudioDeviceInput | AudioFader | AudioListener | AudioEcho
	| AudioAnalyzer | AudioReverb | AudioChorus | AudioFlanger 
	| AudioCompressor | AudioDistortion | AudioEqualizer
	| AudioPitchShifter | AudioFilter | AudioLimiter

which is … definitely an eyesore, but gets access to all the shared methods.

11 Likes

What about simply an AudioBase superclass which means what the instance is doing rather than what it can do?

1 Like

That would work if the only wirable things are audio-related; but we have plans to use Wires for carrying non-audio streams too

Most of the shared/redundant methods are things like :GetConnectedWires which apply to anything that might use wires

4 Likes

I suspected it had to be something like this. That’s frustrating but understandable.
Hopefully interfaces become a thing in the future! Thanks for providing clarity :slight_smile:

5 Likes

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.