Jump to content

Audio Overview

From OpenBOR
Revision as of 01:21, 2 August 2026 by Dcurrent (talk | contribs) (Created page with "OpenBOR uses a unified sample and channel system for sound effects, ambience, voice, and most music playback. WAV and Ogg Vorbis files may be retained in memory or streamed from module data. Music commands remain available as a legacy convenience interface and route WAV or Ogg playback through the ordinary sound channel system. The audio system has three main parts: * '''Sample''' - Loaded audio data or streaming metadata identified by a sample ID. * '''Playback''' - O...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

OpenBOR uses a unified sample and channel system for sound effects, ambience, voice, and most music playback. WAV and Ogg Vorbis files may be retained in memory or streamed from module data. Music commands remain available as a legacy convenience interface and route WAV or Ogg playback through the ordinary sound channel system.

The audio system has three main parts:

  • Sample - Loaded audio data or streaming metadata identified by a sample ID.
  • Playback - One use of a sample, identified by a unique play ID.
  • Channel - Mixer slot that owns the current playback state.

Keeping these concepts separate is important. One loaded sample may play on several channels at once, and every playback receives its own play ID.

Formats

Format Channels Source quality Resident sample Streamed sample Legacy music
PCM WAV Mono or stereo 8, 16, or 24-bit PCM
11.025 through 48 kHz supported
Yes Yes Yes
Ogg Vorbis (.ogg or .oga) Mono or stereo 11.025 through 48 kHz
Decoded to 16-bit PCM
Yes Yes Yes
BOR ADPCM (.bor) Mono or stereo 11.025 through 48 kHz No Legacy music producer Yes

WAV and Ogg sample types are identified by their file signatures rather than the filename extension alone. Valid WAV data begins with RIFF and valid Ogg data begins with OggS. This permits alternate extensions or extensionless paths when the containing command supports them.

Output

OpenBOR currently outputs stereo audio at 16-bit and 44.1 kHz. Source files up to 24-bit and 48 kHz are retained at their source quality when loaded, then converted by the mixer during output. Higher input quality therefore remains available for future output upgrades without another asset conversion.

Recommendations

  • Use WAV for short effects, low decoding overhead, or exact uncompressed PCM.
  • Use Ogg for long sounds where compressed module size is useful.
  • Use 22.05 kHz for compact general-purpose effects when high-frequency detail is unnecessary.
  • Use 44.1 or 48 kHz for music, voice, or effects that benefit from higher-frequency detail.
  • Use mono when a sound will be positioned with separate left and right channel volumes.
  • Use stereo when the source itself contains important left and right information.

Resident and streamed samples

WAV and Ogg samples support two storage modes. The stream argument of loadsample() selects the mode.

Mode stream Load behavior Playback behavior Best use
Resident 0 Loads PCM into memory. Ogg data is decoded during loading. Reads directly from the sample cache. Short or frequently reused effects.
Streamed 1 Loads and validates metadata only. Reads or decodes incrementally through rotating PCM buffers. Music, ambience, speech, and long or infrequently used effects.

The same source file may exist in both modes at once. Resident and streamed forms have separate cache identities and therefore separate sample IDs. Loading the same file repeatedly in the same mode returns its existing sample ID.

Stream behavior

Each active streamed playback owns its file position and, for Ogg, its decoder state. Several channels may therefore stream the same or different files independently.

Current streamed channels retain four rotating buffers of 16 KiB each. At the largest supported WAV input format - 48 kHz, stereo, 24-bit - each buffer holds approximately 57 milliseconds of audio. Total reserve is approximately 57 milliseconds multiplied by the buffer count.

File reads, seeks, decoding setup, allocation, and cleanup occur outside the audio callback. Streamed channels are refilled fairly in round-robin order under a per-update work budget. If storage cannot supply data in time, the mixer outputs silence without advancing past unheard audio.

Stream capacity is not limited by the former eight-handle packfile table. Practical limits are available memory, storage throughput, platform file resources, and the sound channel pool.

Sound channels

OpenBOR exposes channel numbers from 0 through 4095. Internally, channels use 64 banks of 64 channels each.

  • Bank 0, containing channels 0 through 63, is allocated during sound startup.
  • Additional banks are allocated in groups of 64 when existing automatic channels are occupied.
  • Allocated banks remain available until sound shutdown.
  • Finished or stopped playback immediately frees its channel for reuse.
  • 64-bit state masks allow the mixer to skip inactive banks and channels.

Ordinary workloads therefore pay for active playback rather than the full 4,096-channel capacity.

Channel allocation

Automatic playback selects the lowest available non-reserved channel. If every allocated bank is full, OpenBOR allocates the next bank and continues. Priority replacement is considered only when no further automatic channel can be supplied, including allocation failure or exhaustion of the 4,096-channel address space.

When replacement is required, OpenBOR finds the lowest-priority active non-reserved channel:

  • Playback replaces that channel when the new priority is equal to or greater than the existing priority.
  • Playback fails and returns -1 when every replaceable channel has a greater priority.

Priority normally has no effect because almost all projects remain below the available channel capacity.

Channel 0 and music

Channel 0 is soft-reserved for WAV and Ogg music played through the legacy music interface.

  • Automatic sample allocation skips channel 0.
  • Priority replacement skips channel 0.
  • Legacy WAV and Ogg music explicitly targets channel 0.
  • Starting new WAV or Ogg music replaces the current channel 0 playback.
  • Stopping music leaves channel 0 idle but still reserved from automatic allocation.
  • Engine playback paths that explicitly target a channel may use channel 0 because the reservation is an allocator exclusion, not ownership protection.
  • The next legacy music request replaces anything explicitly placed on channel 0.

Bank 0 therefore provides one conventional music channel plus 63 ordinary automatic channels. Total physical capacity remains 4,096 channels.

BOR ADPCM music retains its legacy producer and compatibility channel. WAV and Ogg music use the general channel and streaming systems.

Music

Music commands are compatibility helpers over the audio system. OpenBOR first tries the supplied path exactly. If no playable file is found, it appends extensions in this order:

  1. .bor
  2. .ogg
  3. .oga
  4. .wav

Providing an extension explicitly avoids fallback ambiguity.

Loop offsets

Music offsets select the position used when automatic looping returns from the end of the track. The initial pass still begins at the start.

Music format Offset unit Behavior
WAV or Ogg PCM frame Restarts automatic looping at the selected frame.
BOR ADPCM Encoded data byte Restarts the legacy ADPCM stream at the selected byte offset.

One PCM frame contains every spatial sample for one moment in time. Mono contains one scalar sample per frame, while stereo contains a left and right pair. To convert seconds to a PCM-frame offset:

frame offset = seconds * sample rate

For example, one second into a 48 kHz WAV or Ogg file is frame 48000.

BOR conversion

BOR is a legacy ADPCM music format retained for compatibility and low decoding cost. The wav2bor utility included with the OpenBOR development tools converts WAV sources to BOR music.

Use 16-bit PCM, mono or stereo source WAV files. A 22.05 kHz source remains a practical compatibility choice, while the current decoder accepts 11.025 through 48 kHz.

New projects may generally use Ogg instead unless BOR compatibility or its lower decoding overhead is specifically useful.

Native sound playback

Native model and engine sounds load as resident samples. WAV and Ogg sources are accepted through the same sample loader.

Predefined sounds

OpenBOR loads the following conventional sound paths during startup. Replace a file to customize the effect. Use a silent file when an engine-triggered sound should remain inaudible.

Path Typical use
data/sounds/beat1.wav Default attack impact. Playback speed may vary with damage unless noslowfx is enabled.
data/sounds/block.wav Default blocked-attack impact.
data/sounds/fall.wav Entity landing after a knockdown.
data/sounds/get.wav Normal item pickup.
data/sounds/money.wav Score or money pickup.
data/sounds/jump.wav Jump action.
data/sounds/indirect.wav Indirect collision caused by a thrown or blasted entity.
data/sounds/punch.wav Default standing attack-chain sound, commonly heard on a miss.
data/sounds/1up.wav Extra life.
data/sounds/go.wav Wait completion and movement prompt.
data/sounds/timeover.wav Timer expiration or loss of all credits.
data/sounds/beep.wav Menu navigation.
data/sounds/beep2.wav Menu selection.
data/sounds/pause.wav Pause action. beep2.wav is used as fallback when unavailable.
data/sounds/bike.wav Biker engine effect.

Model commands

diesound

diesound <path>

# Default
diesound none

Sets the resident sample played when the entity is defeated by damage, including special defeat sources such as pits or lifespan expiration.

sound

sound <path>

# Default
sound none

Place sound immediately before an animation frame. The resident sample plays when the next frame is reached. One native sound is supported per frame. Script may start additional sounds when needed.

hitfx and blockfx

hitfx <path>
blockfx <path>

These commands assign resident samples to attack collision data. hitfx plays when the attack hits, while blockfx plays when the attack is blocked. Newer collision-property command names expose the same sound assignments.

Other native systems may select sounds for their own events. Relevant feature articles document those event-specific commands.

Script playback

loadsample()

Loads a resident sample or streaming metadata and returns its sample ID.

int sample_id = loadsample(
    string filename,
    int log_errors,
    int stream
);
Argument Required Default Description
filename Yes None Full module path to a WAV or Ogg source.
log_errors No 0 Nonzero writes failed load attempts to the log.
stream No 0 0 loads resident PCM. Nonzero loads metadata for streamed playback.

Returns -1 when loading fails. Valid sample IDs are zero or greater.

Repeated calls for the same filename and storage mode return the existing sample ID. Resident and streamed forms of the same filename receive different IDs.

playsample()

Starts a sample and returns the selected channel.

int channel = playsample(
    int sample_id,
    unsigned int priority,
    int volume_left,
    int volume_right,
    unsigned int speed,
    int loop,
    mixed start_offset,
    mixed loop_offset
);
Argument Required Default Description
sample_id Yes None Sample ID returned by loadsample() or another engine source.
priority No 0 Replacement priority used only when no automatic channel can be supplied.
volume_left No Current effect volume Left output level. Values are clamped from 0 through 100.
volume_right No Current effect volume Right output level. Values are clamped from 0 through 100.
speed No 100 Playback speed and pitch percentage. Values below 1 use 100.
loop No 0 Nonzero repeats playback until stopped.
start_offset No Legacy start behavior PCM frame used once when playback begins.
loop_offset No 0 PCM frame used when automatic looping restarts after the end.

Returns -1 when playback cannot start. Otherwise, the result is a flattened channel number from 0 through 4095.

Offsets apply equally to resident and streamed WAV or Ogg samples. start_offset never changes later loop behavior, while loop_offset never changes the initial start. Passing an explicit start offset of 0 guarantees playback begins at the first frame. Omitting it preserves OpenBOR's legacy channel-based phase staggering.

The loop offset matters only when loop is nonzero. Both offsets must be smaller than the sample's PCM frame count.

Resident example

int sample_id = loadsample("data/sounds/voice.ogg", 1, 0);
int channel = playsample(sample_id, 0, 100, 100, 100, 0, 0, 0);

Streamed loop example

This example begins at frame zero and loops to one second into a 48 kHz ambience track.

int sample_id = loadsample("data/sounds/ambience.ogg", 1, 1);
int channel = playsample(sample_id, 0, 100, 100, 100, 1, 0, 48000);

Playback and channel control

isactivesample()

int active = isactivesample(int channel);

Returns 1 when the channel contains active playback, or 0 otherwise.

sampleid()

int play_id = sampleid(int channel);

Returns the channel's unique play ID, or -1 when the channel is inactive. Despite the legacy function name, this is not the cached sample ID. Use SOUND_PROPERTY_SAMPLE when the cached sample ID is required.

querychannel()

int channel = querychannel(int play_id);

Returns the active channel containing the supplied play ID, or -1 when that playback is no longer active. This is the inverse lookup for sampleid().

stopchannel()

stopchannel(int channel);

Stops playback on the supplied channel. Stream handles and decoder state are closed outside the audio callback.

pausesample()

pausesample(int toggle, int channel);

Pauses a single active channel when toggle is nonzero and resumes it when zero.

pausesamples()

pausesamples(int toggle);

Pauses or resumes all active sample channels.

unloadsample()

unloadsample(int sample_id);

Releases resident PCM or streamed metadata for the supplied sample ID. The cache identity remains available, so a later loadsample() call for the same filename and mode restores the sample under the same ID when possible.

Avoid unloading a sample while it is still needed by active playback.

Music script interface

Legacy music functions remain available. WAV and Ogg calls use streamed sample playback on channel 0. BOR calls use the legacy ADPCM producer.

playmusic() =

playmusic(string filename, int loop, int loop_offset);

Starts music using the extension search order described above. Calling playmusic() without arguments stops current music.

The optional offset is an automatic-loop position. WAV and Ogg use PCM frames, while BOR uses encoded data bytes.

fademusic() =

fademusic(float fade);
fademusic(float fade, string next_music, int loop, int loop_offset);

Reduces the current music volume by fade during each music fade update. The four-argument form begins the supplied music after volume reaches zero.

setmusicvolume() =

setmusicvolume(int volume_left, int volume_right);

Sets music left and right volume. Omitting volume_right uses the left value for both sides. Normal project values are 0 through 100, while the compatibility path clamps extreme values to 0 through 800.

setmusictempo() =

setmusictempo(int tempo);

Sets music playback speed and pitch as a percentage. 100 is normal playback.

pausemusic() =

pausemusic(int toggle);

Pauses current music when toggle is nonzero and resumes it when zero.

Sound object script API

Advanced scripts may inspect the channel pool, obtain a stable sound object pointer, and read or change playback properties. This API follows the same object and property pattern used by other OpenBOR script objects.

Channel banks are retained after allocation, so a sound object pointer remains structurally valid until sound shutdown. The channel may be reused for later playback, however. Store the play ID when a script must verify that a pointer still represents the same playback instance.

Use named constants from openborconstant(). Raw numeric property and mask IDs are implementation details.

Bank masks

mixed mask = get_sound_channel_bank_mask(int mask_type);

Each returned bit represents one bank. Bit 0 represents bank 0, bit 1 represents bank 1, and so on.

Mask constant Description
SOUND_CHANNEL_BANK_MASK_ALLOCATED Banks whose 64-channel storage has been allocated.
SOUND_CHANNEL_BANK_MASK_ACTIVE Banks containing at least one active channel.
SOUND_CHANNEL_BANK_MASK_AVAILABLE Allocated banks containing at least one channel available to automatic playback.
SOUND_CHANNEL_BANK_MASK_STREAMING Banks containing at least one channel with live streaming state.

Channel masks

mixed mask = get_sound_channel_mask(
    int bank,
    int mask_type
);

Each returned bit represents one slot in the selected 64-channel bank. Convert a bank and local bit position to the public channel number with:

channel = bank * 64 + bit

Unallocated banks return zero for every channel mask.

Mask constant Description
SOUND_CHANNEL_MASK_ACTIVE Channels currently playing or looping.
SOUND_CHANNEL_MASK_PAUSED Active channels currently paused.
SOUND_CHANNEL_MASK_RESERVED Channels excluded from automatic allocation and priority replacement. Channel 0 is reserved by default.
SOUND_CHANNEL_MASK_STREAMING Channels owning a live stream handle or decoder. A finished stream may retain this bit briefly until safe main-thread cleanup.

Mask example

int active_id = openborconstant("SOUND_CHANNEL_MASK_ACTIVE");
mixed bank_zero_active = get_sound_channel_mask(0, active_id);

Channel objects

get_sound_channel_object()

void sound = get_sound_channel_object(int channel);

Returns the stable sound object at a flattened channel index. Valid indexes are 0 through 4095. An unallocated channel returns an empty value.

Allocated but inactive channels still have objects. Test SOUND_PROPERTY_ACTIVE or the active mask before treating an object as current playback.

get_sound_channel_index()

int channel = get_sound_channel_index(void sound);

Validates a sound object pointer and returns its flattened channel index.

Property access

mixed value = get_sound_property(
    void sound,
    int property
);

set_sound_property(
    void sound,
    int property,
    mixed value
);

Playback-sensitive changes use synchronization helpers. Changing the position or loop offset of an active streamed sound discards and rebuilds prefetched buffers so playback resumes from coherent data.

Property constant Type Access Description
SOUND_PROPERTY_ACTIVE Integer Read Activity state: 0 inactive, 1 one-shot playback, 2 looping playback.
SOUND_PROPERTY_CHANNEL Integer Read Flattened channel index from 0 through 4095.
SOUND_PROPERTY_CHANNELS Integer Read Source channel count: 1 mono or 2 stereo.
SOUND_PROPERTY_LOOP_OFFSET Unsigned 64-bit integer Read/write PCM frame used when automatic looping restarts. New values must be within the source.
SOUND_PROPERTY_PAUSED Integer Read/write Pause state. Writes update the authoritative paused mask.
SOUND_PROPERTY_PERIOD Unsigned 64-bit integer Read/write Raw 16.16 fixed-point PCM frames advanced per output frame. Zero is rejected. This is lower-level than the playsample() speed percentage.
SOUND_PROPERTY_PLAY_ID Integer Read Unique ID for the current playback instance.
SOUND_PROPERTY_PRIORITY Integer Read/write Nonnegative replacement priority.
SOUND_PROPERTY_SAMPLE Integer Read Cached sample ID used by the channel.
SOUND_PROPERTY_SAMPLE_POSITION Unsigned 64-bit integer Read/write Current PCM frame. Writing seeks resident or streamed playback without changing the loop offset.
SOUND_PROPERTY_VOLUME_DIVISOR Integer Read/write Gain divisor. Values must be 1 or greater. Lower values increase gain and may cause clipping.
SOUND_PROPERTY_VOLUME_LEFT Integer Read/write Left volume. Writes use normal sample volume clamping from 0 through 100.
SOUND_PROPERTY_VOLUME_RIGHT Integer Read/write Right volume. Writes use normal sample volume clamping from 0 through 100.

Property example

void sound = get_sound_channel_object(channel);

if(sound)
{
    int active = get_sound_property(
        sound,
        openborconstant("SOUND_PROPERTY_ACTIVE")
    );

    if(active)
    {
        set_sound_property(
            sound,
            openborconstant("SOUND_PROPERTY_VOLUME_LEFT"),
            30
        );

        set_sound_property(
            sound,
            openborconstant("SOUND_PROPERTY_VOLUME_RIGHT"),
            100
        );
    }
}

Legacy music channel object

The music_channel openborvariant and get_music_channel_property()/set_music_channel_property() remain available for compatibility. WAV and Ogg music synchronize a compatibility view from sound channel 0, while BOR ADPCM and video audio still use the legacy music producer.

New scripts controlling WAV or Ogg music should normally use get_sound_channel_object(0) and the SOUND_PROPERTY_* API. Legacy buffer pointers do not represent the rotating buffers owned by a generic streamed sound channel.

The following values are available through openborvariant():

Variant Description
effectvol Current effect volume used as the default left and right value for playsample().
musicvol Current music volume.
soundvol Current master sound volume.
maxsoundchannels Maximum flattened sound channel capacity. Current value is 4096.
music_channel Pointer to the legacy music compatibility object.

Practical guidance

  • Preload frequently used resident effects before timing-sensitive gameplay.
  • Stream long voice, ambience, and music that would waste memory when retained as decoded PCM.
  • Keep short, frequently repeated effects resident to avoid unnecessary file and decoder work.
  • Use explicit start offset 0 when exact synchronization to the first PCM frame matters.
  • Store both channel and play ID when later logic must confirm the same playback is still present.
  • Prefer stopchannel(), pausesample(), and property setters over direct assumptions about channel record state.
  • Treat channel masks as fast state summaries. Treat sound objects as reusable channel slots rather than permanent playback objects.
  • Remember that channel 0 is conventional music space, not protected music space.