Flight of a fly
Listen to a buzzing fly wander around a stationary listener. Headphones are recommended: HRTF rendering turns the fly's changing 3D position into clear left, right, front and rear movement.
--[[ Flight of a fly
A small spatial-audio demo. The listener stays still while a buzzing fly
wanders around the listener. Best experienced with
headphones.
]]
local natiny = require("natiny")
local demo = require("common.modules.demo")
-- ── The head ────────────────────────────────────────────────────────────
local HEAD_X, HEAD_Y, HEAD_Z = 0.0, 1.6, 0.0
-- ── The flight ──────────────────────────────────────────────────────────
local FLIGHT_SPEED = 2.0
-- ── The drone ───────────────────────────────────────────────────────────
-- Full volume within SOUND_NEAR - about a hand's width, so a fly at the ear
-- is loud - and quiet but never silent by SOUND_FAR.
local SOUND_NEAR, SOUND_FAR, SOUND_ROLLOFF = 0.25, 20.0, 1.4
local PITCH = 1.0
local BG = { 21 / 255, 30 / 255, 44 / 255 } -- #151E2C
-- Nothing on the command line means AUTO: Direct3D 12 on Windows, Vulkan on
-- Linux, WebGPU on the web.
if not natiny.backend.init(arg and arg[1] or natiny.backend.AUTO) then
os.exit(1)
end
local _, backend_name = natiny.backend.get_current()
print("[fly] backend: " .. backend_name)
local win = natiny.window.create("Natiny - flight of a fly", 1280, 720)
natiny.window.set_scale_mode(win, natiny.window.SCALE_MODE_STRETCH)
-- ── Sound ────────────────────────────────────────────────────────────
local audio = natiny.audio.init()
local source, clip
-- ── Random flight ──────────────────────────────────
math.randomseed(os.time())
local function random_range(minimum, maximum)
return minimum + math.random() * (maximum - minimum)
end
-- Random frequencies and phases make the combined waves non-repeating in
-- practice, while the clock itself always advances at the same rate.
local turn_rate = random_range(0.55, 0.95)
local sway_rate = random_range(0.35, 0.75)
local jitter_rate = random_range(1.1, 1.8)
local radius_rate = random_range(0.25, 0.55)
local radius_jitter_rate = random_range(0.8, 1.4)
local height_rate = random_range(0.4, 0.9)
local height_jitter_rate = random_range(1.0, 1.7)
local phase_a = random_range(0.0, math.pi * 2.0)
local phase_b = random_range(0.0, math.pi * 2.0)
local phase_c = random_range(0.0, math.pi * 2.0)
local phase_d = random_range(0.0, math.pi * 2.0)
local flight_clock = 0.0
local function update_fly(dt)
flight_clock = flight_clock + dt * FLIGHT_SPEED
local angle = flight_clock * turn_rate
+ math.sin(flight_clock * sway_rate + phase_a) * 1.1
+ math.sin(flight_clock * jitter_rate + phase_b) * 0.35
local radius = 1.65
+ math.sin(flight_clock * radius_rate + phase_c) * 0.75
+ math.cos(flight_clock * radius_jitter_rate + phase_a) * 0.30
local height = math.sin(flight_clock * height_rate + phase_d) * 0.65
+ math.sin(flight_clock * height_jitter_rate + phase_b) * 0.22
if source then
natiny.entity.set_position(source, natiny.entity.SPACE_WORLD,
HEAD_X + math.sin(angle) * radius,
HEAD_Y + height,
HEAD_Z + math.cos(angle) * radius)
end
end
-- ── Frame ────────────────────────────────────────────────────────────
-- Nothing to draw but the plate: this demo is what you hear.
local function frame(dt)
natiny.window.bind(win, function()
update_fly(dt)
natiny.render.clear(BG[1], BG[2], BG[3], 1.0)
end)
end
-- The Ogg has landed by the time on_loaded runs, so the clip is only a matter
-- of asking the shell for it.
local function start_sound()
if audio then
local ear = natiny.audio.create_listener()
natiny.entity.set_position(ear, natiny.entity.SPACE_WORLD,
HEAD_X, HEAD_Y, HEAD_Z)
-- The clip is the decoded audio; the shell still holds the Ogg it
-- was decoded from and lets those bytes go after this runs. One
-- clip feeds any number of sources; a swarm would ask for it here
-- exactly like this.
local ok, err = pcall(function()
clip = natiny.clip.load(demo:resource("data/examples/flight_of_a_fly/audio/fly.ogg"))
source = natiny.audio.create_source(clip)
end)
if not ok then
print("[fly] no sound: " .. tostring(err))
source = nil
end
end
-- Keep binaural rendering enabled for the whole demo.
natiny.audio.set_hrtf(true)
if source then
natiny.audio.source_set_spatial(source, true)
natiny.audio.source_set_attenuation(source, natiny.audio.ATTENUATION_INVERSE,
SOUND_NEAR, SOUND_FAR, SOUND_ROLLOFF)
natiny.audio.source_set_loop(source, true)
natiny.audio.source_set_pitch(source, PITCH)
natiny.audio.source_play(source)
print(string.format("[fly] drone: %.1f s, looping, from the fly · HRTF on",
natiny.clip.get_duration(clip)))
elseif audio then
print("[fly] the device is open, but there is nothing to play")
else
print("[fly] no audio output on this machine")
end
demo:set_user_text(source and "Flight of a Fly - headphones recommended"
or "Flight of a Fly - sound unavailable")
end
demo:run({
window = win,
title = "Flight of a fly",
resources = { "data/examples/flight_of_a_fly/audio/fly.ogg" },
on_loaded = start_sound,
loop = frame,
})
natiny.backend.shutdown()