Skip to content

DirectEffect.kt

src/jvmMain/kotlin/afield/audio/spatial/steamaudio/DirectEffect.kt · 260 lines

package afield.audio.spatial.steamaudio

import afield.audio.spatial.steamaudio.raw.IPLDirectEffectParams
import afield.audio.spatial.steamaudio.raw.IPLDirectEffectSettings
import afield.audio.spatial.steamaudio.raw.Phonon
import java.lang.foreign.Arena
import java.lang.foreign.MemorySegment
import java.lang.foreign.ValueLayout

Rendering the direct sound path: distance attenuation, air absorption, directivity, occlusion and transmission.

DirectEffect filters one source's audio per block according to a DirectEffectParams.

public class DirectEffectParams private constructor

Per-block parameters for a DirectEffect, held directly in native memory.

Mutable view over an IPLDirectEffectParams struct: allocate one per source from a long-lived Arena and update it in place each block, so the audio thread allocates nothing.

Only the quantities named in flags are applied, so a parameter you do not set is simply ignored rather than silently attenuating the source.

When driving this from a Simulator, prefer copying the simulator's own output block wholesale via copyFrom rather than transcribing fields one at a time.

public class DirectEffectParams private constructor(
    internal val segment: MemorySegment,
) {

public constructor

Allocates a parameter block from arena.

    public constructor(
        arena: Arena,
        flags: DirectEffectFlags = DirectEffectFlags.NONE,
    ) : this(IPLDirectEffectParams.allocate(arena)) {
        this.flags = flags
        this.transmissionType = TransmissionType.FREQUENCY_INDEPENDENT
        this.distanceAttenuation = 1f
        this.directivity = 1f
        this.occlusion = 1f
        for (band in 0 until SteamAudio.NUM_BANDS) {
            setAirAbsorption(band, 1f)
            setTransmission(band, 1f)
        }
    }

public var flags: DirectEffectFlags

Which of the quantities below are actually applied.

    public var flags: DirectEffectFlags
        get() = DirectEffectFlags(IPLDirectEffectParams.flags(segment))
        set(value) = IPLDirectEffectParams.flags(segment, value.bits)

public var transmissionType: TransmissionType

Whether transmission is applied per-band or as a single broadband gain.

    public var transmissionType: TransmissionType
        get() = TransmissionType.from(IPLDirectEffectParams.transmissionType(segment))
        set(value) = IPLDirectEffectParams.transmissionType(segment, value.value)

public var distanceAttenuation: Float

Linear gain from distance falloff, where 1 is no attenuation.

Applied only when DirectEffectFlags.APPLY_DISTANCE_ATTENUATION is set. Compute it with DistanceAttenuationModel.calculate, or let a Simulator fill it in.

    public var distanceAttenuation: Float
        get() = IPLDirectEffectParams.distanceAttenuation(segment)
        set(value) = IPLDirectEffectParams.distanceAttenuation(segment, value)

public var directivity: Float

Linear gain from the source's directivity pattern, where 1 is omnidirectional.

Applied only when DirectEffectFlags.APPLY_DIRECTIVITY is set.

    public var directivity: Float
        get() = IPLDirectEffectParams.directivity(segment)
        set(value) = IPLDirectEffectParams.directivity(segment, value)

public var occlusion: Float

Fraction of the direct path that reaches the listener: 1 is clear, 0 fully blocked.

Applied only when DirectEffectFlags.APPLY_OCCLUSION is set.

    public var occlusion: Float
        get() = IPLDirectEffectParams.occlusion(segment)
        set(value) = IPLDirectEffectParams.occlusion(segment, value)

public fun setAirAbsorption(band: Int, gain: Float)

Per-band gain from air absorption, where 1 is no absorption.

Applied only when DirectEffectFlags.APPLY_AIR_ABSORPTION is set. band indexes 0 until SteamAudio.NUM_BANDS, low frequency first.

    public fun setAirAbsorption(band: Int, gain: Float) {
        checkBand(band)
        IPLDirectEffectParams.airAbsorption(segment).setAtIndex(
            ValueLayout.JAVA_FLOAT,
            band.toLong(),
            gain,
        )
    }

public fun airAbsorption(band: Int): Float

Reads back a per-band air absorption gain.

    public fun airAbsorption(band: Int): Float {
        checkBand(band)
        return IPLDirectEffectParams.airAbsorption(segment)
            .getAtIndex(ValueLayout.JAVA_FLOAT, band.toLong())
    }

public fun setAirAbsorption(gains: FloatArray)

Copies all SteamAudio.NUM_BANDS air absorption gains from gains.

    public fun setAirAbsorption(gains: FloatArray) {
        require(gains.size == SteamAudio.NUM_BANDS) {
            "expected ${SteamAudio.NUM_BANDS} bands, got ${gains.size}"
        }
        MemorySegment.copy(
            gains, 0,
            IPLDirectEffectParams.airAbsorption(segment), ValueLayout.JAVA_FLOAT, 0L,
            gains.size,
        )
    }

public fun setTransmission(band: Int, gain: Float)

Per-band gain for sound transmitted through geometry.

Applied only when DirectEffectFlags.APPLY_TRANSMISSION is set.

    public fun setTransmission(band: Int, gain: Float) {
        checkBand(band)
        IPLDirectEffectParams.transmission(segment).setAtIndex(
            ValueLayout.JAVA_FLOAT,
            band.toLong(),
            gain,
        )
    }

public fun transmission(band: Int): Float

Reads back a per-band transmission gain.

    public fun transmission(band: Int): Float {
        checkBand(band)
        return IPLDirectEffectParams.transmission(segment)
            .getAtIndex(ValueLayout.JAVA_FLOAT, band.toLong())
    }

public fun setTransmission(gains: FloatArray)

Copies all SteamAudio.NUM_BANDS transmission gains from gains.

    public fun setTransmission(gains: FloatArray) {
        require(gains.size == SteamAudio.NUM_BANDS) {
            "expected ${SteamAudio.NUM_BANDS} bands, got ${gains.size}"
        }
        MemorySegment.copy(
            gains, 0,
            IPLDirectEffectParams.transmission(segment), ValueLayout.JAVA_FLOAT, 0L,
            gains.size,
        )
    }

public fun copyFrom(other: DirectEffectParams)

Copies every field from another parameter block, in one native memory copy.

This is the cheap path for taking a Simulator's direct-path output and handing it straight to a DirectEffect.

    public fun copyFrom(other: DirectEffectParams) {
        segment.copyFrom(other.segment)
    }

    private fun checkBand(band: Int) {
        if (band !in 0 until SteamAudio.NUM_BANDS) {
            throw IndexOutOfBoundsException("band $band outside 0..<${SteamAudio.NUM_BANDS}")
        }
    }
}

public class DirectEffect private constructor

Applies the direct-path acoustics of a source: distance falloff, air absorption, directivity, occlusion and transmission.

This is the non-spatial half of a source's signal chain -- it changes how much and what spectrum arrives, while a BinauralEffect or PanningEffect decides where from. The usual order is direct first, then spatialisation:

direct.apply(directParams, dry, shaped)      // mono -> mono
binaural.apply(binauralParams, shaped, out)  // mono -> stereo

Input and output must both have the channel count the effect was created with.

public class DirectEffect private constructor(
    handle: MemorySegment,
    /** The channel count this effect was created for. */
    public val channelCount: Int,
) : AudioEffect(handle) {

override fun releaseAction(): (MemorySegment) -> Unit = Phonon::iplDirectEffectRelease

override val typeName: String get() = "DirectEffect"

public fun apply

Renders one block. Input and output must both have channelCount channels.

    public fun apply(
        params: DirectEffectParams,
        input: AudioBuffer,
        output: AudioBuffer,
    ): AudioEffectState {
        require(input.channelCount == channelCount && output.channelCount == channelCount) {
            "this direct effect works on $channelCount channels, got " +
                "${input.channelCount} in and ${output.channelCount} out"
        }
        require(input.sampleCount == output.sampleCount) {
            "input and output must be the same length: ${input.sampleCount} vs ${output.sampleCount}"
        }
        return AudioEffectState.from(
            Phonon.iplDirectEffectApply(liveHandle, params.segment, input.struct, output.struct),
        )
    }

override fun reset()

    override fun reset() {
        Phonon.iplDirectEffectReset(liveHandle)
    }

override val tailSize: Int

    override val tailSize: Int
        get() = Phonon.iplDirectEffectGetTailSize(liveHandle)

override fun getTail(output: AudioBuffer): AudioEffectState

    override fun getTail(output: AudioBuffer): AudioEffectState {
        require(output.channelCount == channelCount) {
            "this direct effect works on $channelCount channels, got ${output.channelCount}"
        }
        return AudioEffectState.from(Phonon.iplDirectEffectGetTail(liveHandle, output.struct))
    }

public fun retain(): DirectEffect

Increments the reference count and returns an independently closeable wrapper.

    public fun retain(): DirectEffect =
        DirectEffect(Phonon.iplDirectEffectRetain(liveHandle), channelCount)

public companion object

    public companion object {

public operator fun invoke

Creates a direct effect.

@param channelCount how many channels the effect processes. Mono is the usual choice, since spatialisation happens downstream.

        public operator fun invoke(
            context: SteamAudioContext,
            audioSettings: AudioSettings,
            channelCount: Int = 1,
        ): DirectEffect {
            require(channelCount > 0) { "channelCount must be positive, got $channelCount" }
            val contextHandle = context.liveHandle
            Arena.ofConfined().use { arena ->
                val audio = audioSettings.allocate(arena)
                val settings = IPLDirectEffectSettings.allocate(arena)
                IPLDirectEffectSettings.numChannels(settings, channelCount)
                return DirectEffect(
                    createHandle("iplDirectEffectCreate") { out ->
                        Phonon.iplDirectEffectCreate(contextHandle, audio, settings, out)
                    },
                    channelCount,
                )
            }
        }
    }
}