feat: per-voice audio, outlines for what is not an actor, and a coast
Three things a game could not say, each of which had been worked around. Audio.play_at(id, gain:, pitch:, pan:) fires a one-shot with its own gain, pitch and stereo position. Audio.volume and Audio.pitch are global - they are the options screen - so a game placing a sound in the world was fighting them, and distance attenuation was simply not expressible. The backend already took volume and rate per call; this adds setPan: alongside them and stops routing through the master state. ludic.render3d gains outline_model(model, mat, width, r, g, b): a rim around something that is not an Actor. The outline pass walked the actor list and stopped, so instanced scatter - a forest - could not be highlighted at all. It is a queue flushed by the same pass, which is what gets the depth test right when the caller does not control pass order. And terrain_coast(cx, cz, margin, fall), the other way to make an island: the sea around the survey's own edge rather than cut out of the middle of it. terrain_island measures a radius from a centre, which drowns two thirds of a real survey to make an island of the rest; this measures inward from the boundary, so everything the data covers stays land and the coast is where the data runs out. Both modes gained a strand - the last few metres of height either side of the water line compressed, which stretches a cliff plunge out into beach and shallows. 132 regression tests and the self-host fixpoints pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
39ad8e85d9
commit
3c27606747
15 changed files with 30113 additions and 29690 deletions
14
changes/audio-play-at.md
Normal file
14
changes/audio-play-at.md
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`Audio.play_at(id, gain:, pitch:, pan:)`** — fire a one-shot with its own gain, pitch
|
||||
and stereo position, leaving the master settings alone.
|
||||
|
||||
`Audio.volume` and `Audio.pitch` are global: they are there so a player can turn the game
|
||||
down, and a game that used them to place a sound in the world would be fighting its own
|
||||
options screen. This is the per-voice version, and it is what distance attenuation is made
|
||||
of — a 3D game works out how far away a sound is and which side it is on, and says so.
|
||||
`gain` rides on top of the master volume, so the options screen still wins; `pan` runs
|
||||
-1 (hard left) to 1 (hard right).
|
||||
|
||||
A loaded sound is still one player, so firing the same handle again restarts it rather
|
||||
than layering a second copy. Load a handle per variant when several need to overlap.
|
||||
16
changes/outline-model.md
Normal file
16
changes/outline-model.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic.render3d`: `outline_model(model, mat, width, r, g, b)`** — a rim around something
|
||||
that is not an Actor.
|
||||
|
||||
An Actor has had an `outline` since the outline pass landed, and everything else in a
|
||||
scene had nothing: the pass walked the actor list and stopped. Instanced scatter was the
|
||||
gap that mattered, because a game that highlights whatever the crosshair is on could
|
||||
highlight every object in the world *except* the twenty thousand most common ones — the
|
||||
trees.
|
||||
|
||||
`outline_model` queues a model at a transform from anywhere in the frame and the outline
|
||||
pass flushes it alongside the actors', which is what gets the depth test right: the rim has
|
||||
to be drawn after the scene it is tested against, and a caller does not control pass order.
|
||||
A layer whose vertex shader moves its instances — wind, or standing them on the drawn
|
||||
terrain — should be handed the transform that shader arrives at.
|
||||
17
changes/terrain-coast.md
Normal file
17
changes/terrain-coast.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
bump: minor
|
||||
type: feat
|
||||
**`ludic.render3d`: `terrain_coast(cx, cz, margin, fall)`** — the other way to make an
|
||||
island, and the one a real survey wants: the sea goes around the survey's **own edge**
|
||||
rather than being cut out of the middle of it.
|
||||
|
||||
`terrain_island` measures a radius out from a centre, which suits a made-up map and
|
||||
drowns most of a real one — an 8 km mountain survey loses two thirds of itself to make an
|
||||
island of the rest. `terrain_coast` measures inward from the boundary instead: everything
|
||||
the data covers stays land, the outer `margin` metres go under water, and the `fall` metres
|
||||
inside that are scaled down into it. The band is wobbled by low-frequency noise, so what
|
||||
comes out is headlands and bays rather than the square the data arrived in.
|
||||
|
||||
Both modes also gained a **strand**. Scaling alone hands a 500 m mountainside a 40-degree
|
||||
plunge into the sea, which is a cliff coast and nothing else; the last few metres of height
|
||||
either side of the water line are now compressed, which stretches them out horizontally
|
||||
into beach and shallows. Inland lakes are untouched — they have their own bed.
|
||||
42
docs/language/audio/audio-play_at.md
Normal file
42
docs/language/audio/audio-play_at.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
id: audio-play_at
|
||||
name: Audio.play_at
|
||||
category: audio
|
||||
kind: namespace-method
|
||||
tokens: Audio.play_at
|
||||
sig: Audio.play_at(id, gain, pitch, pan) -> void
|
||||
tip: Fire a one-shot with its own gain, pitch and stereo position.
|
||||
order: 2
|
||||
ns: Audio
|
||||
member: play_at
|
||||
---
|
||||
|
||||
Plays the sound <code>id</code> from the start as a one-shot, with its **own** gain, pitch
|
||||
and stereo position, leaving the master settings alone.
|
||||
|
||||
<code>Audio.volume</code> and <code>Audio.pitch</code> are global: they exist so a player
|
||||
can turn the game down, and a game that used them to place a sound in the world would be
|
||||
fighting its own options screen. This is the per-voice version, and it is what distance
|
||||
attenuation is made of — work out how far away a sound is and which side it is on, and say
|
||||
so here.
|
||||
|
||||
- <code>gain</code> — 0.0 to 1.0, multiplied by the master volume, so the options screen
|
||||
still wins.
|
||||
- <code>pitch</code> — 1.0 is as recorded; 0.5 an octave down, 2.0 an octave up. Clamped
|
||||
to the backend's 0.25 .. 4.0.
|
||||
- <code>pan</code> — -1.0 hard left, 0.0 centred, 1.0 hard right.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
let call = Audio.load("elk.wav")
|
||||
# an elk eighty metres off, over your left shoulder
|
||||
Audio.play_at(id: call, gain: 0.3, pitch: 0.96, pan: -0.55)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
One honest limitation, and it is the backend's: a loaded sound is one player, so firing
|
||||
the same handle again restarts it rather than layering a second copy over the first. Load
|
||||
a handle per variant when several need to overlap — which is what a game does anyway, to
|
||||
stop a repeated sound machine-gunning.
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
# audio.ludic — the Audio.* standard library (#22). Sound effects and music over
|
||||
# the platform audio backend (AVAudioPlayer on macOS). A sound is loaded once
|
||||
# into a handle; sfx fire one-shot, music loops on a single channel; master
|
||||
# volume and pitch apply across everything.
|
||||
# volume and pitch apply across everything, and Audio.play_at fires one shot with
|
||||
# its own gain, pitch and stereo position without touching them.
|
||||
#
|
||||
# Playback is out-of-band — the audio device is real-time, not part of the
|
||||
# deterministic simulation — but every trigger is an ordinary frame-driven call,
|
||||
|
|
@ -21,6 +22,11 @@ program AudioDemo {
|
|||
Audio.volume(0.8) # master volume, 0.0..1.0
|
||||
Audio.pitch(1.25) # playback rate / pitch
|
||||
Audio.play(sfx) # one-shot sound effect
|
||||
# the per-voice version: a sound placed in the world rather than the master settings.
|
||||
# This is what distance attenuation is made of - work out how far away and which side
|
||||
# it is, and say so. gain rides on top of the master volume, so the options screen wins.
|
||||
Audio.play_at(id: sfx, gain: 0.35, pitch: 0.9, pan: -0.6)
|
||||
Audio.play_at(sfx, 0.35, 0.9, -0.6) # or positionally, which a tight loop prefers
|
||||
Audio.play_music(mus) # looping background music
|
||||
print(bi(Audio.is_playing(sfx))) # 0 headless
|
||||
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ Characters and moving objects
|
|||
66. A PPM reader with box-filtered downscale (photo thumbnails) and word-wrapped overlay text — `texture.ludic` `tex_load_ppm`, `overlay.ludic` `ov_text_wrap`
|
||||
67. The overlay batches a whole frame into one upload and draws per texture range with optional scissor clips (per-change uploads stalled the driver: 40 ms -> 25 ms), plus nine-slice, rotated, line, disc and arc primitives — `overlay.ludic` `ov_flush`, `ov_clip`, `ov_nine`, `ov_sub_rot`, `ov_line`, `ov_disc`, `ov_arc`
|
||||
68. Weather on the daylight: an overcast factor greys and dims the sun and the sky's light, a fog multiplier thickens the air, a lightning flash — `daylight.ludic` `daylight_weather`
|
||||
69. An outline pass: an actor carries a rim width and colour, drawn as an inverted hull depth-tested against the scene, and anything that is not an actor can queue one at a transform (a scatter instance, a tile) — `actor.ludic` `actor_draw_outlines`, `outline_model`, `shaders/outline.frag`
|
||||
70. Two ways to make an island out of a survey: a radius from a centre, or the sea put around the survey's own edge with a noise-wobbled coast, both scaling the existing relief into the water and compressing the last few metres of height either side of the line into beach and shallows — `terrain.ludic` `terrain_island`, `terrain_coast`, `shaders/heightgen.frag`
|
||||
|
||||
## Against Unreal Engine 5 and RAGE (RDR2), honestly
|
||||
|
||||
|
|
|
|||
|
|
@ -186,6 +186,44 @@ function actor_draw_one(a: Actor, ap: AcProg, shadow: bool) -> void {
|
|||
}
|
||||
}
|
||||
|
||||
# ---- outlining something that is not an actor ---------------------------------------------
|
||||
# An Actor gets its rim from its own `outline` field. Everything else in a scene - a
|
||||
# scatter instance, a tile, anything drawn by a system that owns its own transforms - had
|
||||
# no way to ask for one, because the outline pass walked the actor list and nothing else.
|
||||
# A tree is the case that matters: instanced scatter has no actor to hull, so a game that
|
||||
# highlights whatever the crosshair is on could highlight every object in the world except
|
||||
# the twenty thousand most common ones.
|
||||
#
|
||||
# So: a queue. `outline_model` says "put a rim round this model at this transform" from
|
||||
# anywhere in the frame, and the outline pass flushes it alongside the actors' - which is
|
||||
# what gets the depth test right, since the rim has to be drawn after the scene it is
|
||||
# tested against and the caller does not control pass order.
|
||||
#
|
||||
# The rim is the model's own geometry swollen along its normals. A layer whose vertex
|
||||
# shader moves its instances - wind, or standing them on the drawn terrain - will differ
|
||||
# from the rim by however much that shader moved them, so pass the transform the shader
|
||||
# would have arrived at (for a grounded layer, the terrain height at that instance).
|
||||
property OutlineReq {
|
||||
model: Model,
|
||||
mat: words,
|
||||
width: int = 0,
|
||||
r: int = 0,
|
||||
g: int = 0,
|
||||
b: int = 0
|
||||
}
|
||||
var ac_oq: []OutlineReq = null
|
||||
var ac_oq_n: int = 0 # live entries; the array is kept and reused
|
||||
function outline_model(m: Model, mat: words, width: int, r: int, g: int, b: int) -> void {
|
||||
if m == null or mat == null or width == 0 { return }
|
||||
if ac_oq == null { ac_oq = new []OutlineReq }
|
||||
var q: OutlineReq = null
|
||||
if ac_oq_n < len(ac_oq) { q = ac_oq[ac_oq_n] } else { q = new OutlineReq; q.mat = m4_new(); push(ac_oq, q) }
|
||||
q.model = m; q.width = width; q.r = r; q.g = g; q.b = b
|
||||
m4_copy(q.mat, mat)
|
||||
ac_oq_n += 1
|
||||
}
|
||||
function outline_clear() -> void { ac_oq_n = 0 }
|
||||
|
||||
function actor_draw() -> void {
|
||||
if ac_actors == null { return }
|
||||
ac_frame += 1
|
||||
|
|
@ -205,7 +243,7 @@ function actor_draw() -> void {
|
|||
# swollen shell survives — a silhouette exactly `outline` metres wide. Depth-tested
|
||||
# against the scene, so anything in front of the actor hides its rim too.
|
||||
function actor_draw_outlines() -> void {
|
||||
var any = false
|
||||
var any = ac_oq_n > 0
|
||||
for i in 0 .. len(ac_actors) { if ac_actors[i].outline != 0 and ac_visible(ac_actors[i], false) { any = true; break } }
|
||||
if not any { return }
|
||||
gl_enable(GL_CULL_FACE)
|
||||
|
|
@ -221,6 +259,19 @@ function actor_draw_outlines() -> void {
|
|||
if a.ocol != null { u_v3(ap.l_ocol, a.ocol) } else { u_f3(ap.l_ocol, F_ONE, F_ONE, F_ONE) }
|
||||
actor_draw_outline_one(a, ap)
|
||||
}
|
||||
# and whatever asked for a rim without being an actor
|
||||
for i in 0 .. ac_oq_n {
|
||||
let q = ac_oq[i]
|
||||
let ap = ac_out
|
||||
gl_use_program(ap.prog)
|
||||
u_mat4(ap.l_view, cam_view); u_mat4(ap.l_proj, cam_proj)
|
||||
u_f(ap.l_out, q.width)
|
||||
u_f3(ap.l_ocol, q.r, q.g, q.b)
|
||||
u_mat4(ap.l_model, q.mat)
|
||||
u_f(ap.l_skin, F_ZERO)
|
||||
for k in 0 .. len(q.model.prims) { mesh_draw(q.model.prims[k].mesh) }
|
||||
}
|
||||
ac_oq_n = 0
|
||||
gl_cull_face(GL_BACK)
|
||||
}
|
||||
function actor_draw_outline_one(a: Actor, ap: AcProg) -> void {
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ uniform float u_dem_base; // the elevation that becomes y = 0
|
|||
uniform vec2 u_origin; // world x/z of the map's centre
|
||||
uniform vec4 u_lake; // a lake: centre x/z, half extents (zero = none)
|
||||
uniform float u_lake_level; // its surface height; the model records the surface, the bed is carved below it
|
||||
uniform vec4 u_isle; // an island: centre x/z, radius, falloff width (radius 0 = none)
|
||||
uniform vec4 u_isle; // an island: centre x/z, radius (or rim margin), falloff width
|
||||
uniform float u_isle_mode; // 0 none, 1 radial (terrain_island), 2 inward from the survey's edge (terrain_coast)
|
||||
// The DEM is Copernicus GLO-30 — 30 m data resampled onto this 4 m grid — so the stored
|
||||
// field is piecewise linear with a slope discontinuity every ~7 texels. Differencing it
|
||||
// for a shading normal turns each kink into a ridge, and on steep ground, where the same
|
||||
|
|
@ -59,16 +60,39 @@ void main() {
|
|||
float bed = u_lake_level - 0.4 - (5.0 + 3.0 * (1.0 - dot(q, q)) + 1.5 * fbm(xz * 0.02, 3)) * basin * basin; // a gentle gravel ramp, then the drop
|
||||
h = mix(h, bed, basin);
|
||||
}
|
||||
// The island. Outside the radius the terrain is scaled toward the water line and then
|
||||
// taken below it, so the shoreline is not drawn on: it is wherever the survey's own
|
||||
// relief, shrunk, happens to cross the water. Low ground becomes beach and shallows,
|
||||
// high ground becomes cliff, and a bay stays a bay.
|
||||
if (u_isle.z > 0.0) {
|
||||
float d = distance(xz, u_isle.xy);
|
||||
float t = smoothstep(u_isle.z, u_isle.z + u_isle.w, d); // 0 inland, 1 past the fall
|
||||
float shelf = 26.0 + 34.0 * smoothstep(0.0, 1.0, (d - u_isle.z - u_isle.w) / max(u_isle.w, 1.0));
|
||||
// The island. The terrain is scaled toward the water line and then taken below it, so
|
||||
// the shoreline is not drawn on: it is wherever the survey's own relief, shrunk, happens
|
||||
// to cross the water. Low ground becomes beach and shallows, high ground becomes cliff,
|
||||
// and a bay stays a bay.
|
||||
//
|
||||
// Two ways of deciding where that happens. RADIAL measures out from a centre, which
|
||||
// suits a made-up map. COAST measures inward from the survey's own boundary, which suits
|
||||
// a real one: everything the data covers stays land and the sea starts where the data
|
||||
// runs out, rather than two thirds of an expensive survey being drowned to make an
|
||||
// island of the rest.
|
||||
if (u_isle_mode > 0.5 && u_isle.z > 0.0) {
|
||||
float t;
|
||||
float shelf;
|
||||
if (u_isle_mode > 1.5) {
|
||||
vec2 e = abs(xz - u_isle.xy);
|
||||
float inland = u_half - max(e.x, e.y); // metres in from the boundary
|
||||
inland += 180.0 * (fbm(xz * 0.00085, 4) * 2.0 - 1.0); // headlands and bays
|
||||
t = 1.0 - smoothstep(u_isle.z, u_isle.z + u_isle.w, inland);
|
||||
shelf = 26.0 + 34.0 * smoothstep(u_isle.z, 0.0, inland);
|
||||
} else {
|
||||
float d = distance(xz, u_isle.xy);
|
||||
t = smoothstep(u_isle.z, u_isle.z + u_isle.w, d); // 0 inland, 1 past the fall
|
||||
shelf = 26.0 + 34.0 * smoothstep(0.0, 1.0, (d - u_isle.z - u_isle.w) / max(u_isle.w, 1.0));
|
||||
}
|
||||
float above = h - u_lake_level;
|
||||
h = u_lake_level + above * (1.0 - t) - t * shelf;
|
||||
// A strand. Scaling alone hands a 500 m mountainside a 40-degree plunge into the sea,
|
||||
// which is a cliff coast and nothing else. Real shores are cut flat by the water they
|
||||
// meet, so the last few metres of height either side of the line are compressed —
|
||||
// which stretches them out horizontally into beach and shallows. Only inside the
|
||||
// coastal band: an inland lake has its own bed and wants none of this.
|
||||
float near = 1.0 - smoothstep(0.0, 48.0, abs(h - u_lake_level));
|
||||
h = mix(h, u_lake_level + (h - u_lake_level) * 0.30, near * smoothstep(0.02, 0.30, t));
|
||||
}
|
||||
o = vec4(h, 0.0, 0.0, 1.0);
|
||||
#elif defined(SMOOTH)
|
||||
|
|
|
|||
|
|
@ -72,12 +72,36 @@ function terrain_lake(level: int, cx: int, cz: int, ex: int, ez: int) -> void {
|
|||
# over `fall` metres and on down to a shelf. Scaling rather than blending to a fixed bed is
|
||||
# what makes the coastline come out of the terrain that is already there — low ground turns
|
||||
# into beach and shallows, high ground into cliff. `r = 0` leaves the survey alone.
|
||||
const TER_ISLE_NONE: int = 0
|
||||
const TER_ISLE_RADIAL: int = 1
|
||||
const TER_ISLE_COAST: int = 2
|
||||
var ter_isle_cx: int = 0
|
||||
var ter_isle_cz: int = 0
|
||||
var ter_isle_r: int = 0
|
||||
var ter_isle_fall: int = 0
|
||||
var ter_isle_mode: int = 0
|
||||
function terrain_island(cx: int, cz: int, r: int, fall: int) -> void {
|
||||
ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = r; ter_isle_fall = fall
|
||||
ter_isle_mode = TER_ISLE_RADIAL
|
||||
if r == 0 { ter_isle_mode = TER_ISLE_NONE }
|
||||
}
|
||||
# The other way to make an island, and the one a real survey usually wants: put the sea
|
||||
# around the survey's OWN EDGE rather than cutting a circle out of the middle of it.
|
||||
# Everything the data covers stays land; the outer `margin` metres are under water and the
|
||||
# `fall` metres inside that are scaled down into it, exactly as terrain_island scales.
|
||||
#
|
||||
# The difference matters more than it sounds. Measuring a radius from a point in the middle
|
||||
# of an 8 km mountain survey drowns most of the survey to make an island of the rest —
|
||||
# you paid for the data and then threw two thirds of it away. Measuring inward from the
|
||||
# boundary keeps all of it and puts the coast where the data runs out, which is also where
|
||||
# a surveyor would tell you it runs out.
|
||||
#
|
||||
# The band is wobbled by low-frequency noise so the coastline is headlands and bays rather
|
||||
# than the square the data arrived in. `margin = 0` leaves the survey alone.
|
||||
function terrain_coast(cx: int, cz: int, margin: int, fall: int) -> void {
|
||||
ter_isle_cx = cx; ter_isle_cz = cz; ter_isle_r = margin; ter_isle_fall = fall
|
||||
ter_isle_mode = TER_ISLE_COAST
|
||||
if margin == 0 { ter_isle_mode = TER_ISLE_NONE }
|
||||
}
|
||||
|
||||
# Use a real place: a 16-bit PNG height map plus its elevation range (metres). The
|
||||
|
|
@ -206,6 +230,7 @@ function terrain_generate() -> void {
|
|||
u_f4(gl_uniform(p, "u_lake"), ter_lake_cx, ter_lake_cz, ter_lake_ex, ter_lake_ez)
|
||||
u_f(gl_uniform(p, "u_lake_level"), ter_lake_level)
|
||||
u_f4(gl_uniform(p, "u_isle"), ter_isle_cx, ter_isle_cz, ter_isle_r, ter_isle_fall)
|
||||
u_f(gl_uniform(p, "u_isle_mode"), fi(ter_isle_mode))
|
||||
u_f(gl_uniform(p, "u_dem_blur"), ter_dem_blur)
|
||||
}
|
||||
mesh_draw(sky_fullscreen)
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
; they vanish from a headless build):
|
||||
; snd_load(path) -> player snd_play(p, loops, rate, vol)
|
||||
; snd_stop(p) snd_playing(p) -> bool
|
||||
; snd_set_volume(p, vol) snd_set_rate(p, rate)
|
||||
; snd_set_volume(p, vol) snd_set_rate(p, rate) snd_set_pan(p, pan)
|
||||
; loops: 0 = once, -1 = forever. rate / vol are 16.16 fixed (value/65536).
|
||||
; ============================================================================
|
||||
|
||||
|
|
@ -37,6 +37,7 @@ declare ptr @objc_msgSend(ptr, ptr, ...)
|
|||
@.a_play = private unnamed_addr constant [5 x i8] c"play\00"
|
||||
@.a_stop = private unnamed_addr constant [5 x i8] c"stop\00"
|
||||
@.a_isply = private unnamed_addr constant [10 x i8] c"isPlaying\00"
|
||||
@.a_span = private unnamed_addr constant [8 x i8] c"setPan:\00"
|
||||
|
||||
; Load a sound file into an AVAudioPlayer; returns the player (or null).
|
||||
define ptr @snd_load(ptr %path) {
|
||||
|
|
@ -137,6 +138,22 @@ out:
|
|||
ret void
|
||||
}
|
||||
|
||||
; Stereo position, -1.0 (hard left) .. 1.0 (hard right). AVAudioPlayer's own pan, so it
|
||||
; costs nothing and needs no mixer: a one-shot can be placed before it is fired.
|
||||
define void @snd_set_pan(ptr %p, i32 %pan) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %p, null
|
||||
br i1 %nil, label %out, label %go
|
||||
go:
|
||||
%pf = sitofp i32 %pan to float
|
||||
%v = fdiv float %pf, 6.5536e+04
|
||||
%sel_p = call ptr @sel_registerName(ptr @.a_span)
|
||||
%rp = call ptr (ptr, ptr, float) @objc_msgSend(ptr %p, ptr %sel_p, float %v)
|
||||
br label %out
|
||||
out:
|
||||
ret void
|
||||
}
|
||||
|
||||
define void @snd_set_rate(ptr %p, i32 %rate) {
|
||||
entry:
|
||||
%nil = icmp eq ptr %p, null
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ const AUDIO_CAP: int = 64 # max simultaneously-loaded sounds
|
|||
|
||||
var snd_ready: bool = false
|
||||
var snd_tab: pointers = null # handle (1-based) -> AVAudioPlayer pointer
|
||||
var snd_master: fixed = 1.0 # master volume, applied to every play
|
||||
var snd_master: fixed = 1.0 # master volume, applied to every play (and to audio_play_at's gain)
|
||||
var snd_rate: fixed = 1.0 # playback rate / pitch (1.0 = normal)
|
||||
var snd_music: int = 0 # the handle currently playing as music (0 = none)
|
||||
# the sound bank: sounds registered by name (Audio.define), played by name (Audio.play(name:))
|
||||
|
|
@ -92,9 +92,42 @@ function audio_play(id: int) -> void {
|
|||
if not is_windowed() { return }
|
||||
let p = audio_get(id)
|
||||
if p == null { return }
|
||||
snd_set_pan(p, 0.0)
|
||||
snd_play(p, 0, snd_rate, snd_master)
|
||||
}
|
||||
|
||||
# Fire a one-shot with its OWN gain, pitch and stereo position, leaving the master
|
||||
# settings alone.
|
||||
#
|
||||
# Audio.volume and Audio.pitch are global: they exist to let a player turn the game down,
|
||||
# and a game that used them to place a sound in the world would be fighting its own
|
||||
# options screen. This is the per-voice version, and it is what distance attenuation is
|
||||
# made of - a 3D game works out how far away and which side a sound is, and says so here.
|
||||
#
|
||||
# gain 0.0 .. 1.0, multiplied by the master volume, so the options screen still wins
|
||||
# pitch 1.0 is as recorded; 0.5 an octave down, 2.0 an octave up
|
||||
# pan -1.0 hard left .. 0.0 centred .. 1.0 hard right
|
||||
#
|
||||
# The one honest limitation: a sound is one player, so firing the same handle again
|
||||
# restarts it rather than layering a second copy. Load a handle per variant when several
|
||||
# need to overlap - which is what a game does anyway to stop a repeated sound machine-gunning.
|
||||
function audio_play_at(id: int, gain: fixed, pitch: fixed, pan: fixed) -> void {
|
||||
if not is_windowed() { return }
|
||||
let p = audio_get(id)
|
||||
if p == null { return }
|
||||
var g = gain
|
||||
if g < 0.0 { g = 0.0 }
|
||||
if g > 1.0 { g = 1.0 }
|
||||
var pn = pan
|
||||
if pn < -1.0 { pn = -1.0 }
|
||||
if pn > 1.0 { pn = 1.0 }
|
||||
var pt = pitch
|
||||
if pt < 0.25 { pt = 0.25 } # AVAudioPlayer's own rate range
|
||||
if pt > 4.0 { pt = 4.0 }
|
||||
snd_set_pan(p, pn)
|
||||
snd_play(p, 0, pt, g * snd_master)
|
||||
}
|
||||
|
||||
# Play a sound as looping background music on the single music channel; any
|
||||
# previous music is stopped first.
|
||||
function audio_play_music(id: int) -> void {
|
||||
|
|
|
|||
|
|
@ -474,6 +474,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
|
|||
# Audio.play / play_music take a handle, or a name from the sound bank (Audio.define)
|
||||
if (meth == "play") { if first_arg_is_text(e) { bare = "audio_play_named"; push(labels, "name") } else { bare = "audio_play"; push(labels, "id") } }
|
||||
if (meth == "play_sound") { bare = "audio_play"; push(labels, "id") }
|
||||
# Audio.play_at(id, gain:, pitch:, pan:) — one shot with its own gain, pitch and
|
||||
# stereo position. Audio.volume / Audio.pitch stay global (they are the options
|
||||
# screen); this is the per-voice one, and it is what distance attenuation is built on.
|
||||
if (meth == "play_at") { bare = "audio_play_at"; push(labels, "id"); push(labels, "gain"); push(labels, "pitch"); push(labels, "pan") }
|
||||
if (meth == "play_music") { if first_arg_is_text(e) { bare = "audio_play_music_named"; push(labels, "name") } else { bare = "audio_play_music"; push(labels, "id") } }
|
||||
if (meth == "stop") { bare = "audio_stop"; push(labels, "id") }
|
||||
if (meth == "stop_music") { bare = "audio_stop_music" }
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ function emit_header() -> void {
|
|||
emith("declare i32 @snd_playing(ptr)\n")
|
||||
emith("declare void @snd_set_volume(ptr, i32)\n")
|
||||
emith("declare void @snd_set_rate(ptr, i32)\n")
|
||||
emith("declare void @snd_set_pan(ptr, i32)\n")
|
||||
emith("declare ptr @hs_req_new(ptr, ptr)\n") # #6 HTTP transport (http.ll)
|
||||
emith("declare void @hs_req_header(ptr, ptr, ptr)\n")
|
||||
emith("declare void @hs_req_body(ptr, ptr, i32)\n")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ function is_intrinsic2(name: pointer) -> bool {
|
|||
if (name == "win_pad") or (name == "win_touch") { return true } # #51 gamepad / touch
|
||||
if (name == "win_cursor_mode") { return true } # #89 cursor capture
|
||||
if (name == "snd_load") or (name == "snd_play") or (name == "snd_stop") { return true } # #22 audio
|
||||
if (name == "snd_playing") or (name == "snd_set_volume") or (name == "snd_set_rate") { return true }
|
||||
if (name == "snd_playing") or (name == "snd_set_volume") or (name == "snd_set_rate") or (name == "snd_set_pan") { return true }
|
||||
if (name == "hs_req_new") or (name == "hs_req_header") or (name == "hs_req_body") { return true } # #6 HTTP
|
||||
if (name == "hs_send") or (name == "hs_done") or (name == "hs_status") or (name == "hs_body") { return true }
|
||||
if (name == "hs_blen") or (name == "hs_header") or (name == "hs_free") { return true }
|
||||
|
|
@ -78,6 +78,7 @@ function emit_intrinsic2(name: pointer, e: Node) -> Val {
|
|||
if (name == "snd_playing") { let a = arg_code(e, 0); return val(emit_bind(`call i32 @snd_playing(ptr {a})`), "int") }
|
||||
if (name == "snd_set_volume") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_volume(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
if (name == "snd_set_rate") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_rate(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
if (name == "snd_set_pan") { let a = arg_code(e, 0); let b = arg_code(e, 1); emit(" call void @snd_set_pan(ptr "); emit(a); emit(", i32 "); emit(b); emit(")\n"); return val("0", "void") }
|
||||
# #6 HTTP transport — NSURLConnection on a background thread (http.ll).
|
||||
if (name == "hs_req_new") { let a = arg_code(e, 0); let b = arg_code(e, 1); return val(emit_bind(`call ptr @hs_req_new(ptr {a}, ptr {b})`), "pointer") }
|
||||
if (name == "hs_req_header") { let a = arg_code(e, 0); let b = arg_code(e, 1); let c = arg_code(e, 2); emit(" call void @hs_req_header(ptr "); emit(a); emit(", ptr "); emit(b); emit(", ptr "); emit(c); emit(")\n"); return val("0", "void") }
|
||||
|
|
|
|||
59522
selfhost/ludicc.seed.ll
59522
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue