commit 034b318059e32c352a02474a68aa5bb1f622ddb7 Author: samstalhandske Date: Sun Feb 8 03:23:18 2026 +0100 Initial commit. diff --git a/audio.odin b/audio.odin new file mode 100644 index 0000000..a8080ca --- /dev/null +++ b/audio.odin @@ -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 +} \ No newline at end of file diff --git a/bus.odin b/bus.odin new file mode 100644 index 0000000..c93311e --- /dev/null +++ b/bus.odin @@ -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 +// } \ No newline at end of file diff --git a/dsp.odin b/dsp.odin new file mode 100644 index 0000000..e14d206 --- /dev/null +++ b/dsp.odin @@ -0,0 +1 @@ +package audio \ No newline at end of file diff --git a/engine/bus.odin b/engine/bus.odin new file mode 100644 index 0000000..f0d03e1 --- /dev/null +++ b/engine/bus.odin @@ -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 +} \ No newline at end of file diff --git a/engine/engine.odin b/engine/engine.odin new file mode 100644 index 0000000..7a3a357 --- /dev/null +++ b/engine/engine.odin @@ -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.. (^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 +} \ No newline at end of file diff --git a/engine/ljud/bus.odin b/engine/ljud/bus.odin new file mode 100644 index 0000000..62c129f --- /dev/null +++ b/engine/ljud/bus.odin @@ -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) +} \ No newline at end of file diff --git a/engine/ljud/ljud.odin b/engine/ljud/ljud.odin new file mode 100644 index 0000000..d20a4b0 --- /dev/null +++ b/engine/ljud/ljud.odin @@ -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) +} \ No newline at end of file diff --git a/engine/ljud/sound.odin b/engine/ljud/sound.odin new file mode 100644 index 0000000..ea2d762 --- /dev/null +++ b/engine/ljud/sound.odin @@ -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 +} \ No newline at end of file diff --git a/engine/sound.odin b/engine/sound.odin new file mode 100644 index 0000000..ad96044 --- /dev/null +++ b/engine/sound.odin @@ -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 +} \ No newline at end of file