Skip to content

SpatialAudioLoopbackSmoke.kt

src/jvmMain/kotlin/afield/audio/engine/SpatialAudioLoopbackSmoke.kt · 75 lines

package afield.audio.engine

import org.lwjgl.openal.AL10.*
import org.lwjgl.openal.ALC10.alcGetCurrentContext
import org.lwjgl.openal.EXTThreadLocalContext.alcGetThreadContext
import org.lwjgl.system.MemoryUtil
import kotlin.math.*

FIELD_TEST id: audio.spatial.loopback runner: jvm-main entrypoint: afield.audio.engine.SpatialAudioLoopbackSmoke tags: [smoke, audio, openal, cpu] timeoutSec: 90 expect: exitCode: 0 stdoutContains: - "SPATIAL_AUDIO_LOOPBACK_PASSED"

object SpatialAudioLoopbackSmoke

Real OpenAL rendering without speakers: context isolation, binaural rotation, Doppler and cleanup.

@JvmStatic fun main(args:Array<String>)

body — 52 lines, from line 23
    @JvmStatic fun main(args:Array<String>) {
        val outer=SimpleSpatialAudioEngine(loopbackSampleRate=48000)
        check(outer.initialize())
        val outerContext=alcGetCurrentContext()
        val nested=SimpleSpatialAudioEngine(threadLocalContext=true,loopbackSampleRate=48000)
        check(nested.initialize())
        check(runCatching { nested.renderSamples(java.nio.ShortBuffer.allocate(128),64) }.exceptionOrNull() is IllegalArgumentException)
        val nestedContext=alcGetThreadContext()
        check(nestedContext!=0L && nestedContext!=outerContext)
        try {
            check(nested.hrtfEnabled) { "Bundled OpenAL did not enable HRTF: ${nested.hrtfStatus}" }
            val inner=SimpleSpatialAudioEngine(threadLocalContext=true,loopbackSampleRate=48000)
            check(inner.initialize());inner.destroy()
            check(alcGetThreadContext()==nestedContext)
            var refused=false
            val wrong=Thread { try { nested.setListener(0f,0f,0f,0f,0f,-1f,0f,1f,0f) } catch(_:IllegalStateException) { refused=true } }
            wrong.start();wrong.join();check(refused)
            val pcm=ShortArray(48000) { (sin(2*PI*1000*it/48000)*9000).toInt().toShort() }
            val buffer=nested.loadPcm16(pcm,48000,1)
            val source=nested.createSource(buffer,rolloffFactor=0f)
            val out=MemoryUtil.memAllocShort(48000)
            fun render(fz:Float,x:Float,z:Float,vz:Float):DoubleArray {
                source.stop();source.seekTo(0f)
                nested.setListener(0f,0f,0f,0f,0f,fz,0f,1f,0f)
                alListener3f(AL_VELOCITY,0f,0f,vz)
                source.setPosition(x,0f,z);source.setVelocity(0f,0f,0f);source.play()
                out.clear();nested.renderSamples(out,24000)
                var l=0.0;var r=0.0;var crosses=0
                for(i in 3000 until 23000) {
                    val a=out.get(i*2).toDouble();val b=out.get(i*2+1).toDouble()
                    l+=a*a;r+=b*b
                    if(a>0 && out.get((i-1)*2)<=0) crosses++
                }
                return doubleArrayOf(l,r,crosses*48000.0/20000)
            }
            try {
                val left=render(-1f,-8f,-2f,0f)
                val turned=render(1f,-8f,-2f,0f)
                check(left[0]>left[1]*1.08 && turned[1]>turned[0]*1.08) { "Head rotation did not exchange ears: ${left.toList()} ${turned.toList()}" }
                val rest=render(-1f,0f,-10f,0f)
                val approach=render(-1f,0f,-10f,-30f)
                check(abs(rest[2]-1000)<8 && approach[2]>rest[2]*1.07) { "Doppler missing: ${rest[2]} -> ${approach[2]}" }
                println("HRTF=${nested.hrtfEnabled}; ear energy ratios=${left[0]/left[1]}, ${turned[1]/turned[0]}; Doppler=${rest[2]} -> ${approach[2]} Hz")
            } finally { MemoryUtil.memFree(out);source.destroy();nested.destroyBuffer(buffer) }
            check(nested.sourceCount==0 && nested.bufferCount==0)
        } finally { nested.destroy() }
        check(alcGetCurrentContext()==outerContext && alcGetThreadContext()==0L)
        // The original engine can still create and use sources after nested context teardown.
        val survivor=outer.createSource();survivor.gain=.25f;check(alGetError()==AL_NO_ERROR);survivor.destroy();outer.destroy()
        println("SPATIAL_AUDIO_LOOPBACK_PASSED: isolated contexts, restored capabilities, thread ownership, HRTF rotation, Doppler, cleanup")
    }
}