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:
Orkun ÇAKILKAYA 2026-09-11 15:25:46 +03:00
parent 39ad8e85d9
commit 3c27606747
15 changed files with 30113 additions and 29690 deletions

14
changes/audio-play-at.md Normal file
View 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
View 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
View 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.

View 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.

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff