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")
-- ── 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 = natiny.color.from_hex("#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)
end)
end
local function start_sound()
if audio then
local ear = natiny.audio.listener_create()
natiny.entity.set_position(ear, natiny.entity.SPACE_WORLD,
HEAD_X, HEAD_Y, HEAD_Z)
-- One clip feeds any number of sources; a swarm would ask for it
-- exactly like this.
local resource = natiny.resource.load("examples/flight_of_a_fly/audio/fly.ogg")
local ok, err = pcall(function()
clip = natiny.clip.load(resource)
source = natiny.audio.source_create(clip)
end)
natiny.resource.destroy(resource)
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
end
start_sound()
natiny.backend.loop(function(dt)
frame(dt)
end)
natiny.backend.shutdown()