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