Initial commit.

This commit is contained in:
2026-02-08 03:23:18 +01:00
commit 034b318059
10 changed files with 845 additions and 0 deletions

136
audio.odin Normal file
View File

@@ -0,0 +1,136 @@
package audio
import "core:log"
import "engine"
Sound :: engine.Sound
Sound_Instance :: engine.Sound_Instance
Bus :: engine.Bus
Listener :: engine.Listener
Context :: struct {
engine: engine.Engine,
}
init :: proc(ctx: ^Context, max_sound_instances: u32) -> bool {
assert(ctx != nil)
ok := engine.init(&ctx.engine, max_sound_instances)
return ok
}
tick :: proc(ctx: ^Context) {
assert(ctx != nil)
engine.tick(&ctx.engine)
}
shutdown :: proc(ctx: ^Context) {
assert(ctx != nil)
engine.shutdown(&ctx.engine)
}
create_bus :: proc(ctx: ^Context) -> (Bus, bool) {
assert(ctx != nil)
bus, ok := engine.create_bus(&ctx.engine)
if !ok {
return {}, false
}
return bus, ok
}
destroy_bus :: proc(ctx: ^Context, bus: ^Bus) {
assert(ctx != nil)
assert(bus != nil)
engine.destroy_bus(&ctx.engine, bus)
}
create_listener :: proc(ctx: ^Context) -> (^Listener, bool) {
assert(ctx != nil)
return engine.create_listener(&ctx.engine)
}
destroy_listener :: proc(ctx: ^Context, listener: ^Listener) {
assert(ctx != nil)
engine.destroy_listener(&ctx.engine, listener)
}
load_sound :: proc {
load_sound_from_path,
}
load_sound_from_path :: proc(ctx: ^Context, path: string) -> (Sound, bool) {
assert(ctx != nil)
sound, ok := engine.load_sound_from_path(&ctx.engine, path)
if !ok {
return {}, false
}
return sound, true
}
unload_sound :: proc(ctx: ^Context, sound: ^Sound) {
assert(ctx != nil)
assert(sound != nil)
engine.unload_sound(&ctx.engine, sound)
}
play_sound :: proc {
play_sound_at_position,
}
play_sound_at_position :: proc(ctx: ^Context, sound: ^Sound, bus: ^Bus, position: [3]f32) -> (^Sound_Instance, bool) {
assert(ctx != nil)
sound_instance, ok := engine.create_sound_instance(&ctx.engine, sound, bus)
if !ok {
log.warnf("Failed to play sound! Failed to create a sound instance.")
return nil, false
}
sound_instance.spatialized = true // TEMP: SS - Hardcoded. Expose!
sound_instance.volume = 1.0 // TEMP: SS - Hardcoded. Expose!
sound_instance.position = position
sound_instance.min_distance = 1.0 // TEMP: SS - Hardcoded. Expose!
sound_instance.max_distance = 50.0 // TEMP: SS - Hardcoded. Expose!
log.infof("Got instance! Now we should 'start'/play it.")
if !engine.play_sound_instance(
&ctx.engine,
sound_instance
) {
log.warnf("Failed to play sound instance.")
return nil, false
}
return sound_instance, true
}
// sound_instance_playing :: proc(sound_instance: ^Sound_Instance) -> bool { // TODO: SS - Implement something like this.
// assert(sound_instance != nil)
// return true // TEMP
// }
update_listener :: proc(ctx: ^Context, listener: ^Listener, position, velocity, direction_forward, world_up: [3]f32) -> bool {
assert(ctx != nil)
assert(listener != nil)
if listener.id == engine.INVALID_LISTENER_ID {
return false
}
listener.position = position
listener.velocity = velocity
listener.direction_forward = direction_forward
listener.world_up = world_up
return true
}

12
bus.odin Normal file
View File

@@ -0,0 +1,12 @@
package audio
import "engine"
// create_bus :: proc(ctx: ^Context) -> (engine.Bus, bool) {
// bus, bus_created := engine.create_bus(&ctx.engine)
// if !bus_created {
// return {}, false
// }
// return bus, true
// }

1
dsp.odin Normal file
View File

@@ -0,0 +1 @@
package audio

49
engine/bus.odin Normal file
View File

@@ -0,0 +1,49 @@
package engine
import "core:log"
import "ljud"
Bus :: struct {
backend: ^Bus_Backend,
}
create_bus :: proc(engine: ^Engine) -> (Bus, bool) {
assert(engine != nil)
bus: Bus
when AUDIO_ENGINE_LJUD {
backend, ok := ljud.create_bus(&engine.backend)
if !ok {
return {}, false
}
bus.backend = backend
}
else {
#assert("TODO: SS - Implement for Audio Engine backend")
}
if bus.backend == nil {
return {}, false
}
return bus, true
}
destroy_bus :: proc(engine: ^Engine, bus: ^Bus) {
assert(engine != nil)
assert(bus != nil)
assert(bus.backend != nil)
when AUDIO_ENGINE_LJUD {
ljud.destroy_bus(&engine.backend, bus.backend)
}
else {
#assert("TODO: SS - Implement for Audio Engine backend")
}
free(bus.backend)
bus.backend = nil
}

132
engine/engine.odin Normal file
View File

@@ -0,0 +1,132 @@
package engine
import "core:container/queue"
import "core:log"
import "core:math/linalg"
AUDIO_ENGINE_LJUD :: #config(AUDIO_ENGINE_LJUD, false)
AUDIO_ENGINE_FMOD :: #config(AUDIO_ENGINE_FMOD, false)
AUDIO_ENGINE_WWISE :: #config(AUDIO_ENGINE_WWISE, false)
import "ljud"
when AUDIO_ENGINE_LJUD {
Engine_Backend :: ljud.Engine
Sound_Backend :: ljud.Sound
Sound_Instance_Backend :: ljud.Sound_Instance
Bus_Backend :: ljud.Bus
}
else when AUDIO_ENGINE_FMOD {
}
Engine :: struct {
backend: Engine_Backend,
sound_instance_ids: queue.Queue(Sound_Instance_ID),
sound_instances: []Sound_Instance,
listeners: [MAX_LISTENERS]Listener,
available_listener_ids: queue.Queue(Listener_ID),
}
init :: proc(engine: ^Engine, max_sound_instances: u32) -> bool {
assert(engine != nil)
assert(max_sound_instances > 0)
when AUDIO_ENGINE_LJUD {
// engine.§ new(ljud.Engine)
// if engine.data != nil {
// v := transmute(^ljud.Engine)(engine.data)
// if !ljud.init(v) {
// free(engine.data)
// engine.data = nil
// log.errorf("Failed to initialize audio-engine LJUD.")
// return false
// }
// }
if !ljud.init(&engine.backend) {
// free(engine.data)
// engine.data = nil
log.errorf("Failed to initialize audio-engine LJUD.")
return false
}
}
else {
#assert("No audio-engine defined. '-define:AUDIO_ENGINE_X=true' where X is the audio-engine; 'LJUD', 'FMOD', 'WWISE'.")
}
{ // Initialize sound-instances.
log.infof("Creating %v sound-instances.", max_sound_instances)
engine.sound_instances = make([]Sound_Instance, max_sound_instances)
queue_init_err := queue.init(&engine.sound_instance_ids, int(max_sound_instances))
assert(queue_init_err == .None)
for i in 0..<len(engine.sound_instances) {
queue.append(&engine.sound_instance_ids, Sound_Instance_ID(i))
}
}
{ // Initialize the array of listeners and the queue of listener-ids.
queue.init(&engine.available_listener_ids, len(engine.listeners))
for i in 0..<len(engine.listeners) {
engine.listeners[i] = Listener {
id = INVALID_LISTENER_ID,
enabled = false,
}
queue.push_back(&engine.available_listener_ids, Listener_ID(i))
}
}
return true
}
tick :: proc(engine: ^Engine) {
when AUDIO_ENGINE_LJUD {
ljud.tick(&engine.backend)
}
else {
#assert("TODO: SS - Implement for Audio Engine backend")
}
// Tick listeners.
for listener in engine.listeners {
if listener.id == INVALID_LISTENER_ID {
continue
}
when AUDIO_ENGINE_LJUD {
ljud.tick_listener(
&engine.backend,
u8(listener.id),
listener.enabled,
listener.position,
listener.velocity,
linalg.normalize(listener.direction_forward),
linalg.normalize(listener.world_up),
)
}
}
}
shutdown :: proc(engine: ^Engine) {
assert(engine != nil)
when AUDIO_ENGINE_LJUD {
ljud.shutdown(&engine.backend)
}
else {
#assert("TODO: SS - Implement for Audio Engine backend")
}
delete(engine.sound_instances)
engine.sound_instances = nil
queue.destroy(&engine.sound_instance_ids)
queue.destroy(&engine.available_listener_ids)
}

64
engine/listener.odin Normal file
View File

@@ -0,0 +1,64 @@
package engine
import "core:log"
import "core:container/queue"
import "ljud"
MAX_LISTENERS :: 4
Listener_ID :: distinct u8
INVALID_LISTENER_ID :: max(Listener_ID)
Listener :: struct {
id: Listener_ID,
enabled: bool,
position, velocity, direction_forward, world_up: [3]f32,
}
create_listener :: proc(
engine: ^Engine,
position: [3]f32 = {},
velocity: [3]f32 = {},
direction_forward: [3]f32 = {},
world_up: [3]f32 = {}
) -> (^Listener, bool)
{
assert(engine != nil)
id, ok := queue.pop_front_safe(&engine.available_listener_ids)
if !ok {
log.warnf("Failed to create listener! No free ID.")
return {}, false
}
listener := &engine.listeners[id]
assert(listener != nil)
listener^ = {
id = id,
enabled = true,
position = position,
velocity = velocity,
direction_forward = direction_forward,
world_up = world_up,
}
return listener, true
}
destroy_listener :: proc(engine: ^Engine, listener: ^Listener) {
assert(engine != nil)
assert(listener != nil)
// Return the ID.
queue.push_back(&engine.available_listener_ids, listener.id)
listener^ = {}
listener.id = INVALID_LISTENER_ID
}

39
engine/ljud/bus.odin Normal file
View File

@@ -0,0 +1,39 @@
package ljud
import "core:log"
import "core:strings"
import ma "vendor:miniaudio"
Bus :: ma.sound_group
create_bus :: proc(engine: ^Engine) -> (^Bus, bool) {
assert(engine != nil)
b := new(Bus)
assert(b != nil)
result := ma.sound_group_init(
pEngine = &engine.ma_engine,
flags = {},
pParentGroup = nil, // TODO: SS - Support parent busses.
pGroup = b,
)
if result != .SUCCESS {
free(b)
return nil, false
}
ma.sound_group_set_spatialization_enabled(b, true) // TODO: SS - Needs to be configurable outside.
ma.sound_group_set_min_distance(b, 1.0) // TODO: SS - Needs to be configurable outside.
ma.sound_group_set_max_distance(b, 50) // TODO: SS - Needs to be configurable outside.
return b, true
}
destroy_bus :: proc(engine: ^Engine, bus: ^Bus) {
assert(engine != nil)
assert(bus != nil)
ma.sound_group_uninit(bus)
}

94
engine/ljud/ljud.odin Normal file
View File

@@ -0,0 +1,94 @@
package ljud
import "core:container/queue"
import "core:log"
import "core:strings"
import ma "vendor:miniaudio"
Engine :: struct {
ma_engine: ma.engine,
}
init :: proc(engine: ^Engine) -> bool {
assert(engine != nil)
log.infof("Initializing LJUD Engine")
engine_config := ma.engine_config_init()
engine_config.channels = 0
engine_config.sampleRate = 0
engine_config.listenerCount = ma.ENGINE_MAX_LISTENERS // HMM: SS - Get this passed in?
res := ma.engine_init(&engine_config, &engine.ma_engine)
if res != .SUCCESS {
log.errorf("Failed to initialize miniaudio! Result: %v.", res)
return false
}
start_res := ma.engine_start(&engine.ma_engine) // TODO: SS - Move elsewhere?
if start_res != .SUCCESS {
log.errorf("Failed to start miniaudio! Result: %v.", start_res)
return false
}
return true
}
tick :: proc(engine: ^Engine) {
assert(engine != nil)
}
tick_listener :: proc(engine: ^Engine, id: u8, enabled: bool, position, velocity, direction_forward, world_up: [3]f32) {
assert(engine != nil)
// Enabled.
ma.engine_listener_set_enabled(
&engine.ma_engine,
u32(id),
b32(enabled),
)
// Position.
ma.engine_listener_set_position(
&engine.ma_engine,
u32(id),
expand_values(position)
)
// Velocity.
ma.engine_listener_set_velocity(
&engine.ma_engine,
u32(id),
expand_values(velocity)
)
// Direction (forward).
ma.engine_listener_set_direction(
&engine.ma_engine,
u32(id),
expand_values(direction_forward)
)
// World (up).
ma.engine_listener_set_world_up(
&engine.ma_engine,
u32(id),
expand_values(world_up)
)
// // Cone. // TODO: SS - Support! :)
// ma.engine_listener_set_cone(
// &engine.ma_engine,
// u32(id),
// ..
// )
// log.infof("Ticked listener %v. Position: %v, velocity: %v, direction forward: %v, world up: %v", id, position, direction_forward, world_up)
}
shutdown :: proc(engine: ^Engine) {
assert(engine != nil)
log.infof("Shutting down LJUD Engine")
ma.engine_uninit(&engine.ma_engine)
}

157
engine/ljud/sound.odin Normal file
View File

@@ -0,0 +1,157 @@
package ljud
import "core:sys/orca"
import "base:runtime"
import "core:log"
import "core:strings"
import "core:c"
import ma "vendor:miniaudio"
Sound :: struct {
// data_source_base: ma.data_source_base,
decoder: ma.decoder,
}
Sound_Instance :: ma.sound
@(private) on_read :: proc "c" (pDataSource: ^ma.data_source, pFramesOut: rawptr, frameCount: u64, pFramesRead: ^u64) -> ma.result {
// Read data here. Output in the same format returned by my_data_source_get_data_format().
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
@(private) on_seek :: proc "c" (pDataSource: ^ma.data_source, frameIndex: u64) -> ma.result {
// Seek to a specific PCM frame here. Return MA_NOT_IMPLEMENTED if seeking is not supported.
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
@(private) on_get_data_format :: proc "c" (pDataSource: ^ma.data_source, pFormat: ^ma.format, pChannels: ^u32, pSampleRate: ^u32, pChannelMap: [^]ma.channel, channelMapCap: c.size_t) -> ma.result {
// Return the format of the data here.
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
@(private) on_get_cursor :: proc "c" (pDataSource: ^ma.data_source, pCursor: ^u64) -> ma.result {
// Retrieve the current position of the cursor here. Return MA_NOT_IMPLEMENTED and set *pCursor to 0 if there is no notion of a cursor.
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
@(private) on_get_length :: proc "c" (pDataSource: ^ma.data_source, pLength: ^u64) -> ma.result {
// Retrieve the length in PCM frames here. Return MA_NOT_IMPLEMENTED and set *pLength to 0 if there is no notion of a length or if the length is unknown.
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
@(private) on_set_looping :: proc "c" (pDataSource: ^ma.data_source, isLooping: b32) -> ma.result {
context = runtime.default_context()
log.infof("%v", #procedure)
return .ERROR
}
load_sound_from_data :: proc(engine: ^Engine, data: []u8, extension: string) -> (^Sound, bool) {
assert(engine != nil)
sound := new(Sound)
assert(sound != nil)
// Configure our decoder
decoder_config := ma.decoder_config_init(
outputFormat = .f32,
outputChannels = 0,
outputSampleRate = 0,
)
// NOTE: SS - "When loading a decoder, miniaudio uses a trial and error technique to find the appropriate decoding backend. This can be unnecessarily inefficient if the type is already known. In this case you can use encodingFormat variable in the device config to specify a specific encoding format you want to decode."
decoder_config.encodingFormat = .unknown // TODO: SS - Check 'extension'.
decoder_result := ma.decoder_init_memory(
pData = raw_data(data),
dataSize = len(data),
pConfig = &decoder_config,
pDecoder = &sound.decoder,
)
if decoder_result != .SUCCESS {
log.errorf("Failed to initialize decoder! Result: %v.", decoder_result)
free(sound)
return nil, false
}
return sound, true
}
unload_sound :: proc(engine: ^Engine, sound: ^Sound) {
assert(engine != nil)
assert(sound != nil)
// assert(sound.data_source != nil)
ma.data_source_uninit(sound.decoder.ds.pCurrent)
}
init_sound_instance :: proc(engine: ^Engine, bus: ^Bus, sound: ^Sound, sound_instance: ^Sound_Instance) -> bool {
result := ma.sound_init_from_data_source(
pEngine = &engine.ma_engine,
pDataSource = sound.decoder.ds.pCurrent,
flags = {},
pGroup = bus,
pSound = sound_instance,
)
if result != .SUCCESS {
log.errorf("Failed to create a sound instance! Result: %v.", result)
return false
}
ma.sound_stop(sound_instance)
return true
}
Play_Sound_Instance_Spec :: struct {
spatialized: bool,
volume: f32,
position: [3]f32,
distance: struct { min, max: f32, }
}
play_sound_instance :: proc(engine: ^Engine, sound_instance: ^Sound_Instance, spec: Play_Sound_Instance_Spec) -> bool {
assert(engine != nil)
assert(sound_instance != nil)
ma.sound_set_spatialization_enabled(sound_instance, b32(spec.spatialized))
ma.sound_set_volume(sound_instance, spec.volume)
ma.sound_set_position(sound_instance, expand_values(spec.position))
ma.sound_set_min_distance(sound_instance, spec.distance.min)
ma.sound_set_max_distance(sound_instance, spec.distance.max)
// TODO: SS - So many cool thingss available! Expose them.
// 'ma.sound_set_looping', 'ma.sound_set_pitch' etc.
seek_result := ma.sound_seek_to_pcm_frame(sound_instance, 0)
if seek_result != .SUCCESS {
log.warnf("Failed to play sound instance! Could not seek to start. Result: %v", seek_result)
return false
}
start_result := ma.sound_start(sound_instance)
if start_result != .SUCCESS {
log.warnf("Failed to play sound instance! Result: %v", start_result)
return false
}
return true
}

161
engine/sound.odin Normal file
View File

@@ -0,0 +1,161 @@
package engine
import os "core:os/os2"
import "core:container/queue"
import "core:log"
import "ljud"
Sound :: struct {
path: string, // Where I was loaded from.
data: []u8,
backend: ^Sound_Backend, // Backend-representation.
}
Sound_Instance :: struct {
parent: ^Sound, // What I'm an instance of.
backend: Sound_Instance_Backend, // Backend-representation.
spatialized: bool,
volume: f32, // 0..1
position: [3]f32,
min_distance, max_distance: f32,
// ..
}
Sound_Instance_ID :: distinct u32
load_sound_from_path :: proc(engine: ^Engine, path: string) -> (Sound, bool) {
assert(engine != nil)
if len(path) == 0 {
return {}, false
}
base, ext := os.split_filename(path)
log.info("Loading sound '%v' with extension '%v'.", base, ext)
// TODO: SS - Check extension.
data, err := os.read_entire_file(path, context.allocator)
if err != nil {
log.errorf("Failed to load sound! Could not read entire file. Error: %v", err)
return {}, false
}
if len(data) == 0 {
log.errorf("Failed to load sound! No data.")
return {}, false
}
when AUDIO_ENGINE_LJUD {
backend, ok := ljud.load_sound_from_data(&engine.backend, data, ext)
if !ok {
return {}, false
}
assert(backend != nil)
log.info("Loaded sound, returning")
return Sound {
path = path,
data = data,
backend = backend,
}, true
}
else when AUDIO_ENGINE_FMOD {
}
else when AUDIO_ENGINE_WWISE {
}
log.info("No engine defined")
return {}, false
}
unload_sound :: proc(engine: ^Engine, sound: ^Sound) {
assert(engine != nil)
assert(sound != nil)
assert(sound.data != nil)
when AUDIO_ENGINE_LJUD {
ljud.unload_sound(&engine.backend, sound.backend)
}
else when AUDIO_ENGINE_FMOD {
}
else when AUDIO_ENGINE_WWISE {
}
delete(sound.data)
sound.data = nil
free(sound.backend)
sound.backend = nil
}
create_sound_instance :: proc(engine: ^Engine, sound: ^Sound, bus: ^Bus) -> (^Sound_Instance, bool) {
assert(engine != nil)
assert(sound != nil)
// TODO: SS - Get an available instance from a pre-allocated buffer.
sound_instance_id, pop_ok := queue.pop_front_safe(&engine.sound_instance_ids)
if !pop_ok {
log.warnf("No free sound-instance available.")
return {}, false
}
sound_instance := &engine.sound_instances[sound_instance_id]
assert(sound_instance != nil)
sound_instance.parent = sound
sound_instance.backend = {}
instance_ok: bool
when AUDIO_ENGINE_LJUD {
instance_ok = ljud.init_sound_instance(&engine.backend, bus != nil ? bus.backend : nil, sound.backend, &sound_instance.backend)
}
else when AUDIO_ENGINE_FMOD {
}
else when AUDIO_ENGINE_WWISE {
}
if !instance_ok {
log.errorf("Failed to create a Sound-Instance for sound '%v'.", sound.path)
return nil, false
}
return sound_instance, true
}
play_sound_instance :: proc(engine: ^Engine, sound_instance: ^Sound_Instance) -> bool {
assert(engine != nil)
assert(sound_instance != nil)
when AUDIO_ENGINE_LJUD {
spec := ljud.Play_Sound_Instance_Spec {
spatialized = sound_instance.spatialized,
volume = sound_instance.volume,
position = sound_instance.position,
distance = {
min = sound_instance.min_distance,
max = sound_instance.max_distance,
},
}
return ljud.play_sound_instance(&engine.backend, &sound_instance.backend, spec)
}
else when AUDIO_ENGINE_FMOD {
}
else when AUDIO_ENGINE_WWISE {
}
return false
}