Modules

Sound_Player

moduleSource

Available on macOS, Linux, Windows

#import "Sound_Player";

Plays sounds: it mixes streams of WAV (PCM or ADPCM) and Ogg Vorbis audio and sends the result to the system's audio output.

Load a sound with load_audio_file or load_audio_data, start the player with sound_player_init, then make a stream with make_stream, set it up and start_playing it. Streams with a position are panned and softened by distance from the listener (update_listener). Call update once per frame; sound_player_shutdown ends it all.

Example
#import "Sound_Player";

sound := load_audio_file("jump.wav");
assert(sound.loaded);
assert(sound_player_init(.{}));

stream := make_stream(*sound, .GENERAL_SFX);
start_playing(stream);

while true {
    update();
    // ... run a frame ...
}

This one is only checked to compile, not run.

Module parameters#

MAX_SOUND_CATEGORIES := DEFAULT_MAX_SOUND_CATEGORIES
How many categories streams can be sorted into.
VERBOSE := false
Logs when the audio output cannot be opened or started.

Procedures#

Samples#

Mixer#

Panning#

(camera_position: Vector3, camera_orientation: Quaternion)

Tells the player where the listener is, so sounds with a position can be panned and softened by distance.

update_listener(.{0, 1.7, 0}, .{0, 0, 0, 1});

This one is only checked to compile, not run.

(value: float)

Sets the volume of everything the player plays, as a factor of the original: 1 is unchanged, 0 is silent.

Cache#

ADPCM#

Decode queue#

Vorbis#

Loading#

(filename: string, optional := false) -> Sound_Data

Reads a sound file. The result has loaded set when it worked.

sound := load_audio_file("music.ogg");
if !sound.loaded return;

This one is only checked to compile, not run.

(name_for_error_reporting: string, data: string) -> Sound_Data

Reads a sound from memory. The result has loaded set when it worked.

sound := load_audio_data("junk", "this is not audio");
assert(!sound.loaded);

Doesn't run in the browser (skipped on wasm).

(data: *Sound_Data)

Stops every stream that plays data, frees the bytes it was loaded from and clears it, so that nothing points into it any more.

samples: [64] s16;
data := Sound_Data.{ loaded = true, type = .LINEAR_SAMPLE_ARRAY, samples = samples.data, nsamples_times_nchannels = samples.count };
data.buffer = copy_string("stands in for the file bytes");

release_sound_data(*data);
assert(!data.loaded && data.buffer.count == 0);

Doesn't run in the browser (skipped on wasm).

(data: *Sound_Data, replacement: Sound_Data) -> bool

Swaps in a new sound for data without moving it: the streams playing the old one are stopped, its bytes are freed and replacement takes its place. Anything that holds the *Sound_Data keeps a valid pointer and gets the new sound from the next make_stream on. This is how to hot-reload a sound.

first := load_audio_data("first", copy_string("not audio"));
assert(!first.loaded); // too short to be a sound; its bytes are still ours
free(first.buffer.data);

samples: [64] s16;
data := Sound_Data.{ loaded = true, type = .LINEAR_SAMPLE_ARRAY, samples = samples.data, nsamples_times_nchannels = samples.count };
data.buffer = copy_string("old bytes");

louder: [64] s16;
louder[0] = 100;
replacement := Sound_Data.{ loaded = true, type = .LINEAR_SAMPLE_ARRAY, samples = louder.data, nsamples_times_nchannels = louder.count };
replacement.buffer = copy_string("new bytes");

assert(replace_sound_data(*data, replacement));
assert(data.samples == louder.data && data.buffer == "new bytes");
release_sound_data(*data);

Doesn't run in the browser (skipped on wasm).

Backend#

backend_count_buffered_bytes3 overloadssince 0.31.0Source
() -> (currently_buffered: s64, minimum_prebuffered: s64)
() -> (currently_buffered: s64, minimum_prebuffered_bytes: s64)
() -> (buffered_bytes: s64, minimum_prebuffered_bytes: s64)

History#

Player#

(given_config: Sound_Player_Config) -> bool

Opens the audio output and starts mixing. Returns false if there is no usable device. Calling it again after it worked does nothing.

if !sound_player_init(.{}) print("no audio\n");

This one is only checked to compile, not run.

()

Stops every stream, closes the output and frees what the player holds. sound_player_init can be called again after it.

(data: *Sound_Data, category := Sound_Category.GENERAL_SFX) -> *Sound_Stream

Makes a stream to play data. Returns null when the sound is not loaded or has more channels than a stream can take.

samples: [64] s16;
sound := Sound_Data.{ loaded = true, type = .LINEAR_SAMPLE_ARRAY, nchannels = 1, samples = samples.data, nsamples_times_nchannels = samples.count };

stream := make_stream(*sound, .UI);
assert(stream != null);

not_loaded: Sound_Data;
assert(make_stream(*not_loaded) == null);

Doesn't run in the browser (skipped on wasm).

(stream: *Sound_Stream)

Gives stream to the player, which plays it from the start and frees it once it ends. Do nothing before sound_player_init.

(entity_id: s64, manager_id: s64) -> *Sound_Stream

The playing stream with this entity_id and manager_id, or null. Set those on a stream to find it later.

sound := load_audio_file("jump.wav");
stream := make_stream(*sound);
stream.entity_id = 7;
start_playing(stream);
assert(find(7, 0) == stream);

This one is only checked to compile, not run.

()

The first half of update: marks streams that have reached their end. Call it and post_entity_update instead of update when you need to touch streams between the two.

(dt: float)

The second half of update: ends streams that were not kept alive, and refreshes the pitch and panning of the rest. dt is the seconds since the last frame.

(dt := FLOAT32_INFINITY)

Advances the player by one frame. Call it once per frame. dt is the seconds since the last call; leave it out to have the player measure it.

while true {
    update();
    // ... draw and handle input ...
}

This one is only checked to compile, not run.

(buffer: string)

Mixes the next stretch of every stream into the output buffer. The audio thread calls it.

(actually_async: bool)

Fills the output with the audio thread's mixing step, or with the main thread's when update_from_a_thread is off.

Windows ABI#

Types#

:: enum

A speaker of the output, as laid out by the player.

FRONT_LEFT
FRONT_RIGHT
CENTER
SUBWOOFER
REAR_LEFT
REAR_RIGHT
EXTEND_LEFT
EXTEND_RIGHT
:: enum

Where a channel of a sound belongs in the room. load_audio_file sets these from the channel count; change Sound_Data.source_directions before starting a stream to override them.

MONO
LEFT
RIGHT
FRONT_LEFT
FRONT_RIGHT
REAR_LEFT
REAR_RIGHT
CENTER_FORWARD
SUBWOOFER_CENTER_FORWARD
EXTEND_LEFT
EXTEND_RIGHT
:: enum

A group of sounds. A stream's category sets its default flags (a .MUSIC stream is music).

GENERAL_SFX
AMBIENCES
FOOTSTEPS
UI
MUSIC
:: struct

A loaded sound. loaded says whether loading worked. volume_scale scales every stream of it, and buffer holds the bytes the sound was read from. Make a Sound_Stream from it to play it.

resource: struct { name: string; full_path: string; loaded := false; skip_load := false; }
type: := Kind.UNINITIALIZED
nsamples_times_nchannels: s64
nchannels: u16
sampling_rate: u32
buffer: string
samples: *s16
volume_scale: float
silence_before_repeat: float
filename_to_stream_from: string
nBlockAlign: s32
wSamplesPerBlock: s32
samples: [8] s16;
sound := Sound_Data.{ loaded = true, type = .LINEAR_SAMPLE_ARRAY, nchannels = 1, samples = samples.data, nsamples_times_nchannels = samples.count };
assert(sound.sampling_rate == 44100);

Doesn't run in the browser (skipped on wasm).

:: struct

One playing instance of a sound. make_stream creates it and start_playing hands it to the player, which owns it from then on. Set user_volume_scale, rate_scale, position and the user_flags before starting it, or any time while it plays.

user_data_u64: u64
user_data_pointer: *void
manager_id: s64
entity_id: s64
sound_name: string
user_flags: enum u32 { REPEATING :: 0x1; SPATIALIZED :: 0x2; MUSIC :: 0x10; VOICEOVER :: 0x20; IS_FOOTSTEP :: 0x40; DO_NOT_KILL :: 0x80; PAUSED_DUE_TO_MENU :: 0x100; WAS_PAUSED_DUE_TO_MENU :: 0x200; AMBIENT :: 0x400; }
internal_flags: enum u32 { FIRST_VOLUME_UPDATE :: 0x2; WAITING_FOR_INITIAL_DECODER_PAGES :: 0x4; FADING_OUT :: 0x8; }
num_channels: s32
sound_data: *Sound_Data
decoder: *Cached_Decoder
category: Sound_Category
repeat_start_position: s64
repeat_end_position: s64
universe_volume_scale: := 1.0
user_volume_scale: := 1.0
play_cursor: float64
silent_samples_at_start_of_play: s64
marked: := false
inaudible: := false
position: Vector3
orientation: Quaternion
inner_radius: := 0.5
outer_radius: := 10.0
rate_scale: := 1.0
samples_streamed_since_entity_update: s64
max_gain: float
last_plane_dir: Vector2
debug_display_volume: float
silent_this_frame: := false
debug_watch: := false
current_rate: float
desired_rate: float
input_scale_mappings: [MAX_CHANNELS_PER_SOUND] Scale_Mapping
initial_samples_fetched: s64
samples_needed_to_start_playing: s64
sound := load_audio_file("jump.wav");
stream := make_stream(*sound);
stream.user_volume_scale = 0.5;
stream.rate_scale = 1.2;  // a little higher in pitch
start_playing(stream);

This one is only checked to compile, not run.

:: struct
pages: [..] *Cached_Decoder_Page
issued_start_addresses: [..] s64
page_size_in_samples: s64
uncompressed_length_in_samples: s64
num_channels: s64
sampling_rate: s64
sound_data: *Sound_Data
stream: *Sound_Stream
do_not_queue: := true
deinit_proc: (decoder: *Cached_Decoder) -> ()
add_decode_queue_item: (page: *Cached_Decoder_Page) -> ()
recompute_page: (decoder: *Cached_Decoder, page: *Cached_Decoder_Page) -> ()
make_page: (decoder: *Cached_Decoder) -> *Cached_Decoder_Page
:: struct

What the audio backend settled on: the channel count and the output sampling rate.

initted: := false
num_channels: s32
channel_names: [] string
output_sampling_rate: s32
:: struct

An audio output that get_devices found.

name: string
hints: [] string
device_index: s64
:: struct

Settings for sound_player_init. The default works: it mixes on its own thread and keeps sounds playing when the window loses focus only if audible_when_window_is_not_in_focus is set. release_asset is called when a stream ends, so the program can free what the stream used.

update_from_a_thread: := true
audible_when_window_is_not_in_focus: := false
keep_sounds_alive_by_default: := true
seconds_to_fill_ahead: := 2.4 / 60.0
set_async_thread_priority: () -> ()
release_asset: (stream: *Sound_Stream, data: *Sound_Data)
config: Sound_Player_Config;
config.update_from_a_thread = false;  // mix inside update() instead
config.audible_when_window_is_not_in_focus = true;
assert(sound_player_init(config));

This one is only checked to compile, not run.

:: enum
HEADPHONE
MONO
QUAD
STEREO
SURROUND
_5POINT1
_5POINT1_BACK
_5POINT1_SURROUND
_7POINT1
_7POINT1_SURROUND
_7POINT1_WIDE
:: struct
GetCaps: (buffer: *IDirectSoundBuffer8, caps: *DSBUFFERCAPS) #c_call
GetCurrentPosition: (buffer: *IDirectSoundBuffer8, pdwCurrentPlayCursor: *s32, pdwCurrentWriteCursor: *s32) -> s32 #c_call
GetFormat: *void
GetVolume: *void
GetPan: *void
GetFrequency: *void
GetStatus: (buffer: *IDirectSoundBuffer8, pdwStatus: *s32) -> s32 #c_call
Initialize: *void
Lock: (buffer: *IDirectSoundBuffer8, dwOffset: s32, dwBytes: s32, pvAudioPtr1: **void, dwAudioBytes1: *s32, pvAudioPtr2: **void, dwAudioBytes2: *s32, dwFlags: s32) -> s32 #c_call
Play: (buffer: *IDirectSoundBuffer8, dwReserved1: s32, dwPriority: s32, dwFlags: s32) -> s32 #c_call
SetCurrentPosition: *void
SetFormat: (buffer: *IDirectSoundBuffer8, format: *Waveformatex) -> s32 #c_call
SetVolume: *void
SetPan: *void
SetFrequency: *void
Stop: (buffer: *IDirectSoundBuffer8) -> s32 #c_call
Unlock: (buffer: *IDirectSoundBuffer8, pvAudioPtr1: *void, dwAudioBytes1: s32, pvAudioPtr2: *void, dwAudioBytes2: s32) -> s32 #c_call
Restore: (buffer: *IDirectSoundBuffer8) -> s32 #c_call
SetFX: *void
AcquireResources: *void
GetObjectInPath: *void

Constants#