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
true when the runtime was compiled with the
voice Cargo feature flag. When false,
GetVoiceChannel errors. Check this before
wiring a mic.
Wrap a SoundData asset (from
Asset.GetAsset("Sound", ...)) into a playable
Player.
Allocate a fresh speaker sink. Default Volume 1.0,
non-spatial.
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.
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.
Build a modifier of the given kind. See the modifier kinds table below for valid strings.
Create a microphone source. Errors with a clear message if
VoiceFlagEnabled = false.
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
Source label inherited from the SoundData.
When true, the source restarts on completion
(fires DidLoop on each wrap). Live-toggleable.
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().
Total source length, decoded from the asset at construction.
Returns 0 for streaming formats with no known
total.
Flips false after :Destroy().
Fires once after each :Play() when audio actually starts emitting.
Fires when playback ends (natural completion or :Stop()).
Fires on each wrap when Looped = true.
Methods
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.
Halt all sinks immediately.
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)
Permanent shutdown: stops all sinks, drops routes, marks
IsAlive = false. Subsequent :Play()
errors.
OutputNode
0 silent, 1 unity, >1 boosts.
Distances ≤ this play at full volume.
Distances ≥ this are silent. Between is a smooth roll-off.
World-space pose. Setting a CFrame switches to 3D spatial
audio (attenuation + pan against the active
Renderable.Camera). Setting nil
reverts to non-spatial.
Convenience setter; equivalent to setting just the position
part of a CFrame. Honored when :Follow() isn't
active.
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.
Drop the follow target. Reverts to CFrame/Position.
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
The OS-default output device name. nil if no
output is configured. Always one of the values returned by
:ListSelectableOutputs.
The device name this OutputNode is currently
pinned to via :SetOutput. Returns nil
while the node is routed through the default device.
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.
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)
Hz of the most recent packet. 0 before any audio has been queued.
How many packets are pending for the next heart tick to fire.
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
Live toggle. When false,
:SendInput is a no-op and the queue stops
growing. Useful for mute UI without tearing down the link.
Sample rate the queued bytes are interpreted at. Set at construction.
Number of interleaved channels (1 for mono, 2 for stereo, …).
Number of f32 samples currently buffered ahead of the playhead.
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
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.
Append a Lua array of raw f32 samples (already decoded). Convenient if you're generating audio in Luau directly.
Empty the buffer without tearing down links. Use when you detect bufferbloat.
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.
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.
Live toggle — flipping to false stops audio flowing but keeps the mic open.
Voice activation amplitude (0…1). When non-zero, audio frames whose peak is below this gate are dropped. Read live from the capture thread.
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)
Stop all sinks and close the mic stream. Capture re-starts automatically if you Link the channel again.
Stop everything and remove the channel from the internal registry. Subsequent use is a no-op.
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
The OS-default input device name. nil if no
input is configured.
The device this VoiceChannel is pinned to via
:SetInput. Returns nil while
capture uses the OS default.
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.
| Kind | Live? | Value units | Notes |
|---|---|---|---|
Volume | live | gain (1 = unity) | Linear amplification. |
Speed | snapshot | multiplier (1 = real-time) | Time + pitch (resampling). |
Pitch | snapshot | multiplier | Alias for Speed. |
PlaybackSpeed | snapshot | multiplier | Alias for Speed. |
Pan | live | −1 … 1 | Stereo pan, sin/cos pan law. |
Distortion | live | 0 … 1 | Tanh saturation. |
LowPass | snapshot | Hz | Biquad lowpass. |
HighPass | snapshot | Hz | Biquad highpass. |
BandPass | snapshot | center Hz | Highpass(value/2) -> Lowpass(value*2). |
Echo | snapshot | delay ms | Single-tap delay with feedback 0.4, mix 0.4. |
Reverb | snapshot | mix 0 … 1 | 4-comb + 2-allpass reverb, decay 0.7. |
Tremolo | live | rate Hz | Amplitude LFO, depth 0.5. |
Vibrato | live | rate Hz | Pitch LFO via tape-delay read-head modulation. |
FadeIn | snapshot | seconds | Linear envelope from start. |
FadeOut | snapshot | seconds | Linear envelope before end. |
Delay | snapshot | seconds | Wait before audio begins. |
BitCrusher | live | bits 1 … 16 | Sample quantization. Low values = chiptune crunch. |
NoiseGate | live | threshold 0 … 1 | Samples below threshold are silenced. |
Compressor | live | threshold 0 … 1 | Envelope-tracking compressor (sqrt ratio). |
Limiter | live | ceiling 0 … 1 | Hard clip at ±ceiling. |
Saturation | live | amount 0 … 1 | Soft warm tanh-style drive. |
RingMod | live | carrier Hz | Multiplies signal by a sine carrier. Robotic / metallic. |
Chorus | live | mix 0 … 1 | 15 ms LFO-modulated delay mixed with dry. |
Flanger | live | mix 0 … 1 | Short modulated delay with feedback. |
StereoWiden | live | width 0 … 2 | Mid/side expansion. Needs ≥ 2 input channels. |
Wobble | live | rate Hz | Heavy amplitude LFO (depth 0.85). Underwater pulsing. |
Telephone | live | drive 0 … 1 | 300 Hz / 3400 Hz band-limit + tanh drive. |
Underwater | live | amount 0 … 1 | 600 Hz lowpass + slow amplitude wobble. |
Mute | live | switch 0 / 1 | Silences 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
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()