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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 12:11:29 +03:00
parent 87587b00b6
commit b2a45c2fda
7 changed files with 74 additions and 2 deletions

View file

@ -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

View file

@ -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)
}

View file

@ -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) } }

View file

@ -5,5 +5,6 @@ numbers float
import "ports.ludic"
import "place.ludic"
import "emit.ludic"
import "channels.ludic"
import "load.ludic"
import "wav.ludic"

View file

@ -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)
}
}

View file

@ -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).

View file

@ -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
}