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.


    
main.lua
--[[ 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()