SoundByte

Graph-routed audio. Sources (Player, VoiceChannel, ByteSource) pass through any number of Modifiers into sinks (OutputNode for speakers, ByteSink for serialized PCM strings you can send over the network or save to disk). The ByteSink → network → ByteSource pair is what builds remote-voice playback — one peer captures, the other plays the bytes back. Replaces the legacy SFX and Voice modules — both still load, but they're frozen.

local SoundByte = import("SoundByte")
local Asset     = import("Asset")

local sfx     = Asset.GetAsset("Sound", "sfx.boom")
local player  = SoundByte.NewPlayer(sfx)
local output  = SoundByte.NewOutput()

local reverb  = SoundByte.GetModifier("Reverb")
reverb.Value = 0.5

local link = SoundByte.Link(player, reverb, output)
player:Play()
-- later:
link:Unlink()
player:Destroy()

SoundByte API

SoundByte.VoiceFlagEnabled boolean read-only

true when the runtime was compiled with the voice Cargo feature flag. When false, GetVoiceChannel errors. Check this before wiring a mic.

SoundByte.NewPlayer(data) → Player

Wrap a SoundData asset (from Asset.GetAsset("Sound", ...)) into a playable Player.

SoundByte.NewOutput() → OutputNode

Allocate a fresh speaker sink. Default Volume 1.0, non-spatial.

SoundByte.NewByte() → ByteSink

Allocate a binary-PCM sink. Wire a source through Link and connect to its .OnPacket signal to receive raw audio bytes you can send over the network or write to disk.

SoundByte.NewByteSource(sampleRate?, channels?) → ByteSource

Allocate a queue-fed audio source. Feed it the byte packets that arrive from a remote ByteSink via :SendInput(packet) and Link it into an OutputNode to play. Defaults are 48000 Hz, 1 channel — pass the sample rate and channels the producing ByteSink was emitting at so the decoded f32 samples come out at the right pitch.

SoundByte.GetModifier(kind) → Modifier

Build a modifier of the given kind. See the modifier kinds table below for valid strings.

SoundByte.GetVoiceChannel() → VoiceChannel

Create a microphone source. Errors with a clear message if VoiceFlagEnabled = false.

SoundByte.Link(source, ...mods, sink) → LinkHandle

Wire a source (Player, VoiceChannel, or ByteSource) through any number of modifiers into a sink (OutputNode or ByteSink). Streaming sources → ByteSink (VoiceChannel → ByteSink, ByteSource → ByteSink) are pumped each tick: samples flow through the modifier chain and out as OnPacket bytes at ~20 ms cadence. Arguments can be in any order; Ruzit identifies each by type. Modifier ordering matters — modifiers apply left-to-right. Multiple Link calls on the same source create independent routes. The returned LinkHandle has :Unlink().

SoundByte.Link(player, lowpass, distortion, reverb, output)

Player

A playable audio source backed by a loaded SoundData. Must be linked to at least one sink before :Play().

Properties & signals

.Sourcestringread-only

Source label inherited from the SoundData.

.Loopedboolean

When true, the source restarts on completion (fires DidLoop on each wrap). Live-toggleable.

.TimePositionnumber (seconds)

Current playback position. Read while playing gives live position (modulo Duration when looped). Write while playing seeks immediately; write while stopped stashes the offset for the next :Play().

.Durationnumber (seconds)read-only

Total source length, decoded from the asset at construction. Returns 0 for streaming formats with no known total.

.IsPlayingbooleanread-only
.IsAlivebooleanread-only

Flips false after :Destroy().

.StartedSignal<>read-only

Fires once after each :Play() when audio actually starts emitting.

.StoppedSignal<>read-only

Fires when playback ends (natural completion or :Stop()).

.DidLoopSignal<>read-only

Fires on each wrap when Looped = true.

Methods

player:Play()method

Begin playback. Errors if no sink is linked. Modifier values captured at Play time for snapshot modifiers (e.g. LowPass); live modifiers (Volume, Pan, Distortion, Tremolo) keep reading their state per-sample.

player:Stop()method

Halt all sinks immediately.

player:LinkToUpdate(interval)→ Signal<number>method

Fires every interval seconds during playback, passing the firing time. Each :Play() resets the clock so the first fire is one full interval in.

player:LinkToUpdate(0.25):Connect(function(t)
    print("quarter-second mark", t)
end)
player:Destroy()method

Permanent shutdown: stops all sinks, drops routes, marks IsAlive = false. Subsequent :Play() errors.

OutputNode

.Volumenumber

0 silent, 1 unity, >1 boosts.

.FalloffMinDistancenumber

Distances ≤ this play at full volume.

.FalloffMaxDistancenumber

Distances ≥ this are silent. Between is a smooth roll-off.

.CFrameCFrame?

World-space pose. Setting a CFrame switches to 3D spatial audio (attenuation + pan against the active Renderable.Camera). Setting nil reverts to non-spatial.

.PositionVector

Convenience setter; equivalent to setting just the position part of a CFrame. Honored when :Follow() isn't active.

output:Follow(part)method

Anchor to a BasePart. Each heart tick the output's position is copied from the part's current CFrame.position. No per-frame Lua needed.

output:Unfollow()method

Drop the follow target. Reverts to CFrame/Position.

output:ListSelectableOutputs()→ { string }method

Enumerate every output device the OS exposes — speakers, headsets, USB DACs, virtual audio cables. The returned strings are the exact names accepted by :SetOutput. Order is host-defined and may change between boots.

for _, name in output:ListSelectableOutputs() do
    print(name)
end
output:GetDefaultOutput()→ string?method

The OS-default output device name. nil if no output is configured. Always one of the values returned by :ListSelectableOutputs.

output:GetCurrentOutput()→ string?method

The device name this OutputNode is currently pinned to via :SetOutput. Returns nil while the node is routed through the default device.

output:SetOutput(name)method

Pin playback to a specific output device. Pass one of the names from :ListSelectableOutputs, or nil to revert to the OS default. The current audio stream is torn down; the next player:Play() opens a fresh stream on the new device. Errors when no device matches the given name.

local headset = findHeadset(output:ListSelectableOutputs())
output:SetOutput(headset)

ByteSink

Audio sink that emits binary PCM packets instead of playing them. Useful for streaming to peers (Steam P2P, Net), saving to disk, or feeding a custom encoder.

.OnPacketSignal<string>read-only

Fires per packet (~20 ms cadence) with a Lua string of interleaved little-endian f32 samples. Decode on the receive side with the same layout.

byte.OnPacket:Connect(function(packet)
    lobby:Broadcast(packet)
end)
.SampleRatenumberread-only

Hz of the most recent packet. 0 before any audio has been queued.

.Channelsnumberread-only
.QueueLengthnumberread-only

How many packets are pending for the next heart tick to fire.

byte:Drain()→ numbermethod

Drop all queued packets. Returns how many were dropped.

ByteSource

The mirror of ByteSink. Where ByteSink turns a live source into a stream of binary PCM packets, ByteSource takes those packets back and plays them as if they were a Player or VoiceChannel. It is the piece you wire on the receiving end of a peer-to-peer voice loop: the remote machine sends bytes over the network, you feed them to :SendInput, and they come out of the OutputNode just like any other source — including routing through modifiers (Reverb, LowPass, StereoWiden, Telephone…) and 3D spatial output via output:Follow(part).

The audio format is the same little-endian f32 layout that ByteSink emits, so in the typical case you just pass the OnPacket string straight to source:SendInput(packet). Construct the source with the same sampleRate and channels the producer is using or playback comes out the wrong pitch.

Properties

.Enabledboolean

Live toggle. When false, :SendInput is a no-op and the queue stops growing. Useful for mute UI without tearing down the link.

.SampleRatenumberread-only

Sample rate the queued bytes are interpreted at. Set at construction.

.Channelsnumberread-only

Number of interleaved channels (1 for mono, 2 for stereo, …).

.QueueLengthnumberread-only

Number of f32 samples currently buffered ahead of the playhead.

.BufferSecondsnumberread-only

QueueLength / (SampleRate * Channels). Read this each tick to detect bufferbloat — if it grows above 0.5 the network is producing faster than the speaker consumes and you may want to :Clear().

Methods

source:SendInput(packet)method

Append a packet of little-endian f32 samples (a Lua string, length must be a multiple of 4). The exact format emitted by ByteSink.OnPacket. The internal queue is capped at ~5 s; older samples are dropped if the producer outpaces the consumer.

source:SendSamples(samples)method

Append a Lua array of raw f32 samples (already decoded). Convenient if you're generating audio in Luau directly.

source:Clear()method

Empty the buffer without tearing down links. Use when you detect bufferbloat.

source:Stop()method

Stop all downstream sinks routed from this source and clear the queue. The LinkHandle is preserved — the next :SendInput after a re-Link starts a fresh playback chain.

source:Destroy()method

Stop and unregister the source. Subsequent :SendInput calls do nothing. Safe to drop the Lua handle afterwards.

Peer voice chat loop

Two machines, one capturing and one playing. The capturing peer builds a Player / VoiceChannel → ByteSink chain; the playing peer builds a ByteSource → OutputNode chain and feeds every received packet into :SendInput.

-- ============ Sender (mic owner) ============
local mic   = SoundByte.GetVoiceChannel()
local byte  = SoundByte.NewByte()
SoundByte.Link(mic, byte)

byte.OnPacket:Connect(function(packet)
    lobby:SendToPeer("voice", packet)
end)

-- ============ Receiver (everyone else) ============
local source = SoundByte.NewByteSource(48000, 1)
local output = SoundByte.NewOutput()
local reverb = SoundByte.GetModifier("Reverb")
reverb.Value = 0.15

SoundByte.Link(source, reverb, output)

lobby.OnVoicePacket:Connect(function(_peer, packet)
    source:SendInput(packet)
end)

-- Optional: 3D positional voice when the speaker has an avatar.
output:Follow(peerCharacter.Head)

ByteSource composes with every modifier and with OutputNode:Follow, so positional voice, telephone-style effects, or whisper / shout gain staging all work out of the box. The source has no cpal dependency — it works on builds without the voice feature, so receivers don't need a microphone device.

VoiceChannel

Microphone source. Construct with SoundByte.GetVoiceChannel() (requires the voice Cargo feature). Capture starts automatically when you Link the channel to a sink; it stops when the last link is removed.

.Enabledboolean

Live toggle — flipping to false stops audio flowing but keeps the mic open.

.Thresholdnumber

Voice activation amplitude (0…1). When non-zero, audio frames whose peak is below this gate are dropped. Read live from the capture thread.

.VolumeLevelnumberread-only

Live peak amplitude of the most recent mic buffer (0…1), smoothed with a one-pole filter (60 / 40 weight). Use this to build threshold UIs, push-to-talk indicators, or your own gating logic on top of Threshold.

RunService.Heartbeat:Connect(function()
    if mic.VolumeLevel > 0.08 then
        chatBubble.Visible = true
    end
end)
.SampleRatenumberread-only
.Channelsnumberread-only
.IsCapturingbooleanread-only
mic:Stop()method

Stop all sinks and close the mic stream. Capture re-starts automatically if you Link the channel again.

mic:Destroy()method

Stop everything and remove the channel from the internal registry. Subsequent use is a no-op.

mic:ListSelectableInputs()→ { string }method

Enumerate every input device the OS exposes — physical microphones, USB headsets, virtual audio cables. Names are the exact strings accepted by :SetInput.

for _, name in mic:ListSelectableInputs() do
    print(name)
end
mic:GetDefaultInput()→ string?method

The OS-default input device name. nil if no input is configured.

mic:GetCurrentInput()→ string?method

The device this VoiceChannel is pinned to via :SetInput. Returns nil while capture uses the OS default.

mic:SetInput(name)method

Pin capture to a specific input device. Pass one of the names from :ListSelectableInputs, or nil to revert to the OS default. The current capture stream (if any) is torn down; a new one starts on the next Link / capture trigger. The named device is verified lazily — if it doesn't exist when capture starts, voice_ensure_capture errors with no input device named '...'.

mic:SetInput("Headset (USB)")
SoundByte.Link(mic, output) -- capture opens on the named device

Modifier

A single effect node in the graph. Each Modifier has one Kind (set at construction, immutable), an Enabled toggle, and three numeric properties: Min, Max, Value. Value is the active parameter; setting it auto-clamps to [Min, Max]. Setting Min above Max (or vice versa) auto-corrects the other side.

Tweenable. All three numeric properties (Value, Min, Max) are valid targets for TweenService.new.

Modifier kinds

Some adapters run as live DSP (re-read Value per sample, so tweens take effect mid-playback). Others are snapshot adapters that capture the value at :Play() time — a tween while playing won't affect them, but the next Play picks up the new value.

KindLive?Value unitsNotes
Volumelivegain (1 = unity)Linear amplification.
Speedsnapshotmultiplier (1 = real-time)Time + pitch (resampling).
PitchsnapshotmultiplierAlias for Speed.
PlaybackSpeedsnapshotmultiplierAlias for Speed.
Panlive−1 … 1Stereo pan, sin/cos pan law.
Distortionlive0 … 1Tanh saturation.
LowPasssnapshotHzBiquad lowpass.
HighPasssnapshotHzBiquad highpass.
BandPasssnapshotcenter HzHighpass(value/2) -> Lowpass(value*2).
Echosnapshotdelay msSingle-tap delay with feedback 0.4, mix 0.4.
Reverbsnapshotmix 0 … 14-comb + 2-allpass reverb, decay 0.7.
Tremololiverate HzAmplitude LFO, depth 0.5.
Vibratoliverate HzPitch LFO via tape-delay read-head modulation.
FadeInsnapshotsecondsLinear envelope from start.
FadeOutsnapshotsecondsLinear envelope before end.
DelaysnapshotsecondsWait before audio begins.
BitCrusherlivebits 1 … 16Sample quantization. Low values = chiptune crunch.
NoiseGatelivethreshold 0 … 1Samples below threshold are silenced.
Compressorlivethreshold 0 … 1Envelope-tracking compressor (sqrt ratio).
Limiterliveceiling 0 … 1Hard clip at ±ceiling.
Saturationliveamount 0 … 1Soft warm tanh-style drive.
RingModlivecarrier HzMultiplies signal by a sine carrier. Robotic / metallic.
Choruslivemix 0 … 115 ms LFO-modulated delay mixed with dry.
Flangerlivemix 0 … 1Short modulated delay with feedback.
StereoWidenlivewidth 0 … 2Mid/side expansion. Needs ≥ 2 input channels.
Wobbleliverate HzHeavy amplitude LFO (depth 0.85). Underwater pulsing.
Telephonelivedrive 0 … 1300 Hz / 3400 Hz band-limit + tanh drive.
Underwaterliveamount 0 … 1600 Hz lowpass + slow amplitude wobble.
Muteliveswitch 0 / 1Silences while Enabled and Value ≥ 0.5; passes through otherwise. Default Value = 0, so a freshly linked Mute is a no-op until you flip Value = 1. Tween-friendly — sweep Value through 0.5 for a hard cut.

LinkHandle

link:Unlink()method

Detach the route. Stops the specific sink created by this Link (other routes from the same source keep working). For VoiceChannel sources: when the last link is removed the cpal mic stream is dropped cleanly.

Build flag

The VoiceChannel half of SoundByte requires the voice Cargo feature, which pulls in cpal (mic capture) and opus (compression used by the legacy Voice module for back-compat). Player / OutputNode / ByteSink / Modifier all work in the default build.

Check SoundByte.VoiceFlagEnabled at runtime and bail gracefully if false:

if not SoundByte.VoiceFlagEnabled then
    print("voice disabled in this build")
    return
end
local mic = SoundByte.GetVoiceChannel()