---
title: "soundClip"
description: "An audio asset — a managed container holding a sound effect or music track, addressed by the .soundClip suffix."
section: "Types"
slug: "types-soundclip"
canonical: "https://origozero.ai/docs/types-soundclip"
updated: "2026-09-07T03:15:14.927920239+00:00"
tags: ["asset-type", "reference"]
---

# soundClip

`soundClip` is a first-class assetType (not a bare plural-dir category) so
that:

- a soundClip has a stable asset **identity** and an explicit `asset_type`
  link, and
- a component that plays a soundClip records a real `.refs` edge to it.

## Internal layout

A `.soundClip/` is permissive (`allow_unlisted: true`) — the conversion step
decides its shape. A managed container holds:

```
<name>.soundClip/
  source.<ext>   the original audio (.ogg / .mp3 / .wav / .flac), MOVED in.
                 Present for imported clips; absent for a PCM-baked clip.
  .metadata      compression settings — see below
  data.zaud      engine-native ZAUD payload the runtime decodes + plays, under
                 the codec its own header names
  README.md
```

`data.zaud` is the **preferred primary**: when present the runtime resolves
and decodes it directly. When absent the primary falls through to the source
audio file, so imported-only containers keep working.

## `.metadata` settings

Editing any of these re-runs encoding — against the sibling `source.<ext>`
when the container has one, or against the decoded `data.zaud` for a
PCM-baked clip — and rewrites `data.zaud`:

| key | values | meaning |
|-----|--------|---------|
| `codec` | `opus` (default) / `pcm` | output codec. `opus` compresses; `pcm` stores uncompressed samples. |
| `bitrateKbps` | integer, default `96` | Opus target bitrate (ignored for `pcm`). |
| `vbr` | `true` (default) / `false` | Opus variable bitrate. |
| `sampleRate` | integer, `0` = keep source | resample target in Hz. |
| `forceMono` | `false` (default) / `true` | downmix to a single channel. |
| `loadType` | `auto` (default) / `decompressOnLoad` / `streaming` | how the runtime loads the clip. |
| `loopStart` | integer frames, `0` = none | loop-point start. |
| `loopEnd` | integer frames, `0` = none | loop-point end. |

The `data.zaud` payload is produced by the engine's audio codec, exposed to
Luau as `audio.encode` / `audio.encodePcm` / `audio.decode` / `audio.info` /
`audio.loopSeam` and consumed by this assetType.

Every sample the codec is handed must be a finite number, so `asset.create`
raises when the `pcm` or `bytes` it is given carries a NaN or an infinity, and
the error names how many samples fail and where the first one sits.

## Does it loop without a click

Whole-clip repeat is the loop the runtime plays, so the join between a clip's
last frame and its first is heard once a lap. `clipRef:loopSeam()` reads what
the samples do there: the step the wrap makes (`|x[1] - x[frames]|`) against
the step the signal ordinarily makes between neighbouring samples, as
`ratio = step / meanStep`. The figure is in the units the signal itself moves
in, so a generated ambience travelling a thousandth of a unit per sample and a
synth drone travelling a hundredth are read the same way.

| field | meaning |
|-------|---------|
| `ratio` | `step / meanStep` on the worst channel — one channel clicking is the clip clicking. |
| `step` | `\|x[1] - x[frames]\|`, the step the wrap makes. |
| `meanStep` | the mean `\|x[i + 1] - x[i]\|`, the distance the signal ordinarily travels in one sample. |
| `maxStep` | the largest `\|x[i + 1] - x[i]\|` the channel already carries. |
| `channel` | which channel the four figures above came from, counted from 1. |
| `seamless` | `ratio <= threshold`. |
| `threshold` | the ratio the engine warns past — `8`. |
| `channels` | every channel's own `{ ratio, step, meanStep, maxStep }`, in channel order. |

A bed whose partials complete a whole number of cycles across the buffer reads
near 1; one carrying a percussive strike at its head and silence at its tail
reads in the tens.

```lua
local seam = clipRef:loopSeam()
if not seam.seamless then
    print(seam.ratio, seam.step, seam.meanStep)  -- e.g. 41.0  0.5993  0.0146
end
```

`asset.create("soundClip", ...)` takes the same reading of the payload it just
encoded and logs a warning naming the ratio when it is past `threshold`, so a
bed that ticks is reported at the call that bakes it. The re-encodes a source
edit and a settings change trigger report it the same way.

The reading is taken on the DECODED payload, so it answers for what the codec
left behind rather than for the buffer that was handed to the encoder — and
for a clip that arrived already encoded, from an import or from ZeroMind, whose
source buffer nobody holds.

## Creating a soundClip

`asset.create("soundClip", name, opts)` reads six keys of its own out of `opts`,
and the settings table above is one of them. `asset.create`'s framework keys —
`folder`, `into`, `dest` and `overwrite` — apply to a soundClip the way they
apply to every type.

| parameter | type | meaning |
|-----------|------|---------|
| `bytes` | `string?` | encoded source audio (OGG / MP3 / WAV / FLAC), stored verbatim as `source.<ext>`. |
| `ext` | `"ogg"` (default) / `"mp3"` / `"wav"` / `"flac"` | which container `bytes` is in. |
| `pcm` | `buffer \| string \| { number }?` | interleaved f32 samples — a `buffer`, the shape `microphone.samples` hands back; a binary string of little-endian f32, the shape `audio.decode` returns; or a flat number array. |
| `sampleRate` | `number?` | the rate those samples are at, in Hz. Required with `pcm`. |
| `channels` | `number?` | how many channels they interleave. Required with `pcm`. |
| `settings` | `table?` | the full settings table from the section above, baked in the same pass; wins over the defaults. |

```lua
-- from encoded audio bytes (kept as source.<ext>, encoded to data.zaud)
asset.create("soundClip", "music", { bytes = oggBytes, ext = "ogg" })

-- from raw PCM samples (no source file; encoded straight to data.zaud)
local samples = buffer.create(960 * 4)
for i = 0, 959 do
    buffer.writef32(samples, i * 4, math.sin(i * 0.05) * 0.5)
end
asset.create("soundClip", "beep", { pcm = samples, sampleRate = 48000, channels = 1 })

-- the same samples kept exactly as handed in, under a codec and a load
-- strategy chosen in the one pass that bakes them
asset.create("soundClip", "bed", {
    pcm = samples, sampleRate = 48000, channels = 1,
    settings = { codec = "pcm", loadType = "decompressOnLoad" },
})
```

A `buffer` is the cheap container for a long clip: a 1.5 s mono 48 kHz clip is
72 000 samples, which is a 288 KB buffer against a 72 000-entry Luau table.

**Choose the codec at create time.** Encoding runs once per bake, and a later
settings change re-encodes from what is already stored — `source.<ext>` when the
container has one, and otherwise the decoded `data.zaud`. So a clip baked with
the default `opus` and then given `setSettings({ codec = "pcm" })` stores the
Opus signal losslessly rather than the samples that were handed in: the frame
count, the duration and the settings table all read as asked for, and only the
samples differ. Passing `settings` to `asset.create` bakes the samples once,
under the codec they are meant to keep.

Read a clip's settings with `clipRef:settings()`, patch them with
`clipRef:setSettings({ bitrateKbps = 64 })`, decode its samples with
`clipRef:pcm()`, and read its loop point with `clipRef:loopSeam()`.

`clipRef:pcm()` hands back `(pcm, sampleRate, channels)` — the same triple
`audio.decode` returns. The samples come back packed rather than as a Luau
array: a binary string of interleaved little-endian f32, four bytes per sample,
so `#pcm // 4` is the sample count and one sample reads as
`string.unpack("<f", pcm, i * 4 + 1)` or through `buffer.fromstring(pcm)`.

```lua
local pcm, sampleRate, channels = clipRef:pcm()
local frames = #pcm // 4 // channels
print(frames / sampleRate, "seconds")           -- e.g. 1.5  seconds

local peak = 0
for i = 0, #pcm // 4 - 1 do
    peak = math.max(peak, math.abs(string.unpack("<f", pcm, i * 4 + 1)))
end
```

The second and third returns are the clip's own rate and channel count, so a
duration is arithmetic on what `pcm()` alone hands back. `audio.info` reads the
same figures off the stored payload without decoding it, and the `topics/audio`
guide lists every field it returns.

The `topics/audio` guide covers synthesis end to end: computing a buffer,
baking it into a clip, playing it from an `Audio` component, and reading it back.
