From b2a45c2fda9881ea57a061a87d5eb64ab86e7d5f Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 12:11:29 +0300 Subject: [PATCH] audio: a volume per channel under the master (effects, wildlife, ambient, interface) - aud_channel_set / aud_emit_on, aud_ui on the interface channel, and Audio.music_volume for the ambience loop on the runtime's music channel Co-Authored-By: Claude Opus 5.5 --- packages/ludic.audio/README.md | 7 ++++++ packages/ludic.audio/channels.ludic | 24 +++++++++++++++++++ packages/ludic.audio/emit.ludic | 3 ++- packages/ludic.audio/index.ludic | 1 + .../ludic.audio/tests/channels_test.ludic | 23 ++++++++++++++++++ runtime/native/audio.ludic | 17 ++++++++++++- runtime/native/namespaces.ludic | 1 + 7 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 packages/ludic.audio/channels.ludic create mode 100644 packages/ludic.audio/tests/channels_test.ludic diff --git a/packages/ludic.audio/README.md b/packages/ludic.audio/README.md index 3a7d8887..2f7d7197 100644 --- a/packages/ludic.audio/README.md +++ b/packages/ludic.audio/README.md @@ -18,6 +18,13 @@ aud_emit_at(axe, tree.x, tree.z, 90.0) # heard within 90 m, pa aud_ui(click) ``` +## Channels + +A volume per kind of sound, under the master: `AUD_CH_EFFECTS` (what the player does), `AUD_CH_WILDLIFE`, +`AUD_CH_AMBIENT` (its loop is the runtime's music channel, `Audio.music_volume`) and `AUD_CH_UI` +(`aud_ui` plays on it). `aud_channel_set(ch, v)` holds it to 0..1; `aud_emit_on(ch, h, ...)` is +`aud_emit` scaled by it. `aud_emit` itself stays raw, so its numbers are what a test reads. + ## Ports ```ludic diff --git a/packages/ludic.audio/channels.ludic b/packages/ludic.audio/channels.ludic new file mode 100644 index 00000000..a284c451 --- /dev/null +++ b/packages/ludic.audio/channels.ludic @@ -0,0 +1,24 @@ +# channels.ludic - a volume per kind of sound, under the master: what the player does, the +# animals, the ambience (its loop is the runtime's music channel) and the interface +export const AUD_CH_EFFECTS: int = 0 +export const AUD_CH_WILDLIFE: int = 1 +export const AUD_CH_AMBIENT: int = 2 +export const AUD_CH_UI: int = 3 +export const AUD_CH_N: int = 4 + +export function aud_channel(audio_st: AudioState, ch: int) -> float { + if ch < 0 or ch >= AUD_CH_N { return 1.0 } + return audio_st.ch_gain[ch] +} + +export function aud_channel_set(audio_st: mut AudioState, ch: int, v: float) -> void { + if ch < 0 or ch >= AUD_CH_N { return } + let g = Math.clamp(v, 0.0, 1.0) + audio_st.ch_gain[ch] = g + if ch == AUD_CH_AMBIENT { Audio.music_volume(fixed(g)) } +} + +# a sound in the world on a channel: its gain from where it is, scaled by the channel's +export function aud_emit_on(audio_st: mut AudioState, ch: int, h: int, gain: float, pitch: float, pan: float) -> void { + aud_emit(audio_st, h, gain * aud_channel(audio_st, ch), pitch, pan) +} diff --git a/packages/ludic.audio/emit.ludic b/packages/ludic.audio/emit.ludic index fcc456f8..835c655b 100644 --- a/packages/ludic.audio/emit.ludic +++ b/packages/ludic.audio/emit.ludic @@ -8,6 +8,7 @@ export state AudioState { emitted: int = 0 tried: int = 0 live: int = 0 + ch_gain: []float = [1.0, 1.0, 1.0, 1.0] # per channel (channels.ludic), under the master } export function aud_emit(audio_st: mut AudioState, h: int, gain: float, pitch: float, pan: float) -> void { @@ -32,7 +33,7 @@ export function aud_emit_at(audio_st: mut AudioState, h: int, x: float, z: float export function aud_ui(audio_st: mut AudioState, h: int) -> void { if h == 0 { return } audio_st.emitted += 1 - Audio.play(h) + Audio.play_at(h, fixed(aud_channel(audio_st, AUD_CH_UI)), fixed(1.0), fixed(0.0)) } export function aud_music(h: int) -> void { if h != 0 { Audio.play_music(h) } } diff --git a/packages/ludic.audio/index.ludic b/packages/ludic.audio/index.ludic index ac625f4b..80e45347 100644 --- a/packages/ludic.audio/index.ludic +++ b/packages/ludic.audio/index.ludic @@ -5,5 +5,6 @@ numbers float import "ports.ludic" import "place.ludic" import "emit.ludic" +import "channels.ludic" import "load.ludic" import "wav.ludic" diff --git a/packages/ludic.audio/tests/channels_test.ludic b/packages/ludic.audio/tests/channels_test.ludic new file mode 100644 index 00000000..f0e826f7 --- /dev/null +++ b/packages/ludic.audio/tests/channels_test.ludic @@ -0,0 +1,23 @@ +# channels_test.ludic - a channel scales what is emitted on it, holds to 0..1, and leaves the others +import "ludic.audio" +program ChannelsTest { + numbers float + function t_x() -> float { return 0.0 } + bind AudioListener { x: fn t_x, z: fn t_x } + + test "a channel's volume scales an emit on it and no other" (audio_st: mut AudioState) { + aud_channel_set(audio_st, AUD_CH_WILDLIFE, 0.5) + aud_emit_on(audio_st, AUD_CH_WILDLIFE, 7, 0.8, 1.0, 0.0) + expect_near(aud_last_gain(audio_st), 0.4, 0.0001) + aud_emit_on(audio_st, AUD_CH_EFFECTS, 7, 0.8, 1.0, 0.0) + expect_near(aud_last_gain(audio_st), 0.8, 0.0001) + } + + test "a channel holds to 0..1, and an unknown one is full" (audio_st: mut AudioState) { + aud_channel_set(audio_st, AUD_CH_UI, 3.0) + expect_near(aud_channel(audio_st, AUD_CH_UI), 1.0, 0.0001) + aud_channel_set(audio_st, AUD_CH_AMBIENT, -1.0) + expect_near(aud_channel(audio_st, AUD_CH_AMBIENT), 0.0, 0.0001) + expect_near(aud_channel(audio_st, 9), 1.0, 0.0001) + } +} diff --git a/runtime/native/audio.ludic b/runtime/native/audio.ludic index f439ace3..11094457 100644 --- a/runtime/native/audio.ludic +++ b/runtime/native/audio.ludic @@ -25,6 +25,7 @@ export state RtAudioState { snd_master: fixed = 1.0 # master volume, applied to every play (and to audio_play_at's gain) snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal) snd_music: int = 0 # the handle currently playing as music (0 = none) + snd_music_gain: fixed = 1.0 # the music channel's own volume, under the master snd_bank_name: pointers = null snd_bank_id: words = null snd_bank_n: int = 0 @@ -138,7 +139,7 @@ function audio_play_music(rt_audio_st: mut RtAudioState, id: int) -> void { if p == null { return } audio_stop_music(rt_audio_st) rt_audio_st.snd_music = id - snd_play(p, -1, rt_audio_st.snd_rate, rt_audio_st.snd_master) + snd_play(p, -1, rt_audio_st.snd_rate, rt_audio_st.snd_master * rt_audio_st.snd_music_gain) } # Stop one sound. @@ -183,6 +184,20 @@ function audio_volume(rt_audio_st: mut RtAudioState, v: fixed) -> void { if rt_audio_st.snd_tab[i] != null { snd_set_volume(rt_audio_st.snd_tab[i], v) } i += 1 } + audio_music_volume(rt_audio_st, rt_audio_st.snd_music_gain) +} + +# The music channel's own volume (0.0 .. 1.0), under the master: a game's ambience loop has a +# slider of its own without scaling every one-shot with it. +function audio_music_volume(rt_audio_st: mut RtAudioState, v: fixed) -> void { + var g = v + if g < 0.0 { g = 0.0 } + if g > 1.0 { g = 1.0 } + rt_audio_st.snd_music_gain = g + if not is_windowed() { return } + if rt_audio_st.snd_music == 0 { return } + let p = audio_get(rt_audio_st, rt_audio_st.snd_music) + if p != null { snd_set_volume(p, rt_audio_st.snd_master * g) } } # Playback rate / pitch (1.0 = normal, 0.5 = an octave down, 2.0 = up). diff --git a/runtime/native/namespaces.ludic b/runtime/native/namespaces.ludic index 2b0fc065..59b52f5d 100644 --- a/runtime/native/namespaces.ludic +++ b/runtime/native/namespaces.ludic @@ -178,6 +178,7 @@ namespace Audio { alias stop_music() = audio_stop_music alias stop_all() = audio_stop_all alias volume(v) = audio_volume + alias music_volume(v) = audio_music_volume # the music channel under the master alias pitch(v) = audio_pitch alias is_playing(id) = audio_is_playing }