Simulation.kt
src/jvmMain/kotlin/afield/audio/spatial/steamaudio/Simulation.kt · 626 lines
package afield.audio.spatial.steamaudio
Steam Audio's acoustic simulator: SimulationSettings fixes its quality and cost, Simulator
runs direct, reflection and pathing simulation over a Scene, and each Source carries its
own SimulationInputs in and SimulationOutputs out.
import afield.audio.spatial.steamaudio.raw.IPLCoordinateSpace3
import afield.audio.spatial.steamaudio.raw.IPLSimulationInputs
import afield.audio.spatial.steamaudio.raw.IPLSimulationOutputs
import afield.audio.spatial.steamaudio.raw.IPLSimulationSettings
import afield.audio.spatial.steamaudio.raw.IPLSimulationSharedInputs
import afield.audio.spatial.steamaudio.raw.IPLSourceSettings
import afield.audio.spatial.steamaudio.raw.IPLVector3
import afield.audio.spatial.steamaudio.raw.Phonon
import java.lang.foreign.Arena
import java.lang.foreign.MemorySegment
import java.lang.foreign.ValueLayout
public data class SimulationSettings
How the simulator should be configured. Fixed for the simulator's lifetime.
The numbers here are the main quality/cost dial for a reverberant space. Notes on the ones that matter most:
maxDurationis how long an impulse response the simulator will produce. For a cave, this is the reverb length you are asking for -- 2 to 4 seconds is a big space. Cost scales with it, and a ReflectionEffect fed by this simulator must be created with at least this duration.maxNumRaysand numBounces (the latter set per-frame via SimulationSharedInputs) control how thoroughly the space is explored. Sparse rays in a large dark space means the simulation misses surfaces and the reverb flickers as the listener moves. This is the setting to raise first if the space sounds unstable.maxOrderis the Ambisonic order of the reflected field. Order 1 is adequate for a diffuse tail; order 2 sharpens early reflections at 9 channels instead of 4.numThreadsshould leave a core free for the audio thread.
public data class SimulationSettings(
/** Which simulation stages this simulator supports. */
public val flags: SimulationFlags = SimulationFlags.DIRECT or SimulationFlags.REFLECTIONS,
/** Ray tracer backend. Must match the [Scene] type. */
public val sceneType: SceneType = SceneType.DEFAULT,
/** Reflection algorithm. Must match the [ReflectionEffect] type. */
public val reflectionType: ReflectionEffectType = ReflectionEffectType.CONVOLUTION,
/** Maximum occlusion samples for volumetric occlusion. */
public val maxNumOcclusionSamples: Int = 16,
/** Maximum rays cast per reflection simulation. */
public val maxNumRays: Int = 4096,
/** Diffuse samples taken per ray bounce. */
public val numDiffuseSamples: Int = 1024,
/** Longest impulse response the simulator will produce, in seconds. */
public val maxDuration: Float = 2.0f,
/** Ambisonic order of the reflected field. */
public val maxOrder: Int = 1,
/** How many sources may be simulated at once. */
public val maxNumSources: Int = 8,
/** Worker threads for reflection simulation. */
public val numThreads: Int = (Runtime.getRuntime().availableProcessors() - 1).coerceAtLeast(1),
/** Rays per batch, for the Radeon Rays backend. */
public val rayBatchSize: Int = 1,
/** Visibility samples per probe pair, for pathing. */
public val numVisSamples: Int = 4,
/** Sampling rate, matching your [AudioSettings]. */
public val samplingRate: Int = 48_000,
/** Block size, matching your [AudioSettings]. */
public val frameSize: Int = 1024,
) {
init
init {
require(maxDuration > 0f) { "maxDuration must be positive, got $maxDuration" }
require(maxOrder >= 0) { "maxOrder cannot be negative, got $maxOrder" }
require(maxNumSources > 0) { "maxNumSources must be positive, got $maxNumSources" }
require(numThreads > 0) { "numThreads must be positive, got $numThreads" }
require(maxNumRays > 0) { "maxNumRays must be positive, got $maxNumRays" }
}
public val reflectionChannelCount: Int get() = SteamAudio.ambisonicChannelCount(maxOrder)
Number of Ambisonic channels the reflected field will have.
internal fun allocate
internal fun allocate(
arena: Arena,
openClDevice: OpenClDevice?,
radeonRaysDevice: RadeonRaysDevice?,
trueAudioNextDevice: TrueAudioNextDevice?,
): MemorySegment {
val segment = IPLSimulationSettings.allocate(arena)
IPLSimulationSettings.flags(segment, flags.bits)
IPLSimulationSettings.sceneType(segment, sceneType.value)
IPLSimulationSettings.reflectionType(segment, reflectionType.value)
IPLSimulationSettings.maxNumOcclusionSamples(segment, maxNumOcclusionSamples)
IPLSimulationSettings.maxNumRays(segment, maxNumRays)
IPLSimulationSettings.numDiffuseSamples(segment, numDiffuseSamples)
IPLSimulationSettings.maxDuration(segment, maxDuration)
IPLSimulationSettings.maxOrder(segment, maxOrder)
IPLSimulationSettings.maxNumSources(segment, maxNumSources)
IPLSimulationSettings.numThreads(segment, numThreads)
IPLSimulationSettings.rayBatchSize(segment, rayBatchSize)
IPLSimulationSettings.numVisSamples(segment, numVisSamples)
IPLSimulationSettings.samplingRate(segment, samplingRate)
IPLSimulationSettings.frameSize(segment, frameSize)
IPLSimulationSettings.openCLDevice(segment, openClDevice.handleOrNull())
IPLSimulationSettings.radeonRaysDevice(segment, radeonRaysDevice.handleOrNull())
IPLSimulationSettings.tanDevice(segment, trueAudioNextDevice.handleOrNull())
return segment
}
}
public class SimulationSharedInputs private constructor
Inputs shared by every source in a simulation: where the listener is, and how hard to work.
A mutable native view, so updating it per frame allocates nothing.
public class SimulationSharedInputs private constructor(
internal val segment: MemorySegment,
/** Arena used for the pathing-visualisation upcall stub, when one is installed. */
private val stubArena: Arena,
) {
public constructor(arena: Arena) : this(IPLSimulationSharedInputs.allocate(arena), arena)
Allocates a shared-inputs block from arena.
public constructor(arena: Arena) : this(IPLSimulationSharedInputs.allocate(arena), arena) {
listener = CoordinateSpace3.IDENTITY
numRays = 4096
numBounces = 16
duration = 2.0f
order = 1
irradianceMinDistance = 1.0f
IPLSimulationSharedInputs.pathingVisCallback(segment, MemorySegment.NULL)
IPLSimulationSharedInputs.pathingUserData(segment, MemorySegment.NULL)
}
public var listener: CoordinateSpace3
The listener's position and orientation in world space.
public var listener: CoordinateSpace3
get() = CoordinateSpace3.readFrom(IPLSimulationSharedInputs.listener(segment))
set(value) = value.writeTo(IPLSimulationSharedInputs.listener(segment))
public fun setListenerPosition(x: Float, y: Float, z: Float)
Sets just the listener's position, leaving the orientation alone.
Avoids constructing a CoordinateSpace3 when only the position has moved.
public fun setListenerPosition(x: Float, y: Float, z: Float) {
val origin = IPLCoordinateSpace3.origin(IPLSimulationSharedInputs.listener(segment))
IPLVector3.x(origin, x)
IPLVector3.y(origin, y)
IPLVector3.z(origin, z)
}
public var numRays: Int
Rays cast per reflection simulation. Must not exceed the simulator's maxNumRays.
In a large, mostly empty space, too few rays means surfaces get missed and the reverb wobbles frame to frame. Raise this before anything else if the space sounds unstable.
public var numRays: Int
get() = IPLSimulationSharedInputs.numRays(segment)
set(value) = IPLSimulationSharedInputs.numRays(segment, value)
public var numBounces: Int
How many times each ray may bounce.
A reverberant cave needs a lot of bounces -- the late tail is the high-order reflections. Values in the tens are normal for a live space; a handful will sound dead.
public var numBounces: Int
get() = IPLSimulationSharedInputs.numBounces(segment)
set(value) = IPLSimulationSharedInputs.numBounces(segment, value)
public var duration: Float
Impulse response duration to simulate, in seconds. Capped by the simulator's maxDuration.
public var duration: Float
get() = IPLSimulationSharedInputs.duration(segment)
set(value) = IPLSimulationSharedInputs.duration(segment, value)
public var order: Int
Ambisonic order to simulate. Capped by the simulator's maxOrder.
public var order: Int
get() = IPLSimulationSharedInputs.order(segment)
set(value) = IPLSimulationSharedInputs.order(segment, value)
public var irradianceMinDistance: Float
Minimum distance used when accumulating irradiance, which stops a ray that grazes a surface from contributing an unbounded amount of energy.
public var irradianceMinDistance: Float
get() = IPLSimulationSharedInputs.irradianceMinDistance(segment)
set(value) = IPLSimulationSharedInputs.irradianceMinDistance(segment, value)
public fun setPathingVisualizationCallback(callback: PathingVisualizationCallback?)
Installs a callback that receives every probe-to-probe segment the pathing search
examines, or clears it when callback is null.
The stub lives in the arena this object was constructed with, so that arena must outlive any simulation run using these inputs. Intended for debugging -- see PathingVisualizationCallback for why it is worth wiring up in a 2D game, and for the constraints on what the callback may do.
public fun setPathingVisualizationCallback(callback: PathingVisualizationCallback?) {
if (callback == null) {
IPLSimulationSharedInputs.pathingVisCallback(segment, MemorySegment.NULL)
} else {
IPLSimulationSharedInputs.pathingVisCallback(
segment,
PathingVisualizationCallbacks.stub(callback, stubArena),
)
}
IPLSimulationSharedInputs.pathingUserData(segment, MemorySegment.NULL)
}
}
public class SimulationInputs private constructor
Per-source simulation inputs. A mutable native view.
public class SimulationInputs private constructor(
internal val segment: MemorySegment,
) {
public constructor
Allocates a per-source inputs block from arena.
public constructor(
arena: Arena,
flags: SimulationFlags = SimulationFlags.DIRECT or SimulationFlags.REFLECTIONS,
directFlags: DirectSimulationFlags = DirectSimulationFlags.DISTANCE_ATTENUATION or
DirectSimulationFlags.AIR_ABSORPTION,
) : this(IPLSimulationInputs.allocate(arena)) {
this.flags = flags
this.directFlags = directFlags
source = CoordinateSpace3.IDENTITY
occlusionType = OcclusionType.RAYCAST
occlusionRadius = 1.0f
numOcclusionSamples = 16
numTransmissionRays = 1
hybridReverbTransitionTime = 0.4f
hybridReverbOverlapPercent = 0.25f
baked = false
enableValidation = false
findAlternatePaths = false
visRadius = 1.0f
visThreshold = 0.1f
visRange = 50.0f
pathingOrder = 1
for (band in 0 until SteamAudio.NUM_BANDS) setReverbScale(band, 1f)
// Models default to Steam Audio's own; callers can override.
DistanceAttenuationModel.DEFAULT.writeInto(IPLSimulationInputs.distanceAttenuationModel(segment))
AirAbsorptionModel.DEFAULT.writeInto(IPLSimulationInputs.airAbsorptionModel(segment))
Directivity.OMNIDIRECTIONAL.writeInto(IPLSimulationInputs.directivity(segment))
IPLSimulationInputs.pathingProbes(segment, MemorySegment.NULL)
IPLSimulationInputs.deviationModel(segment, MemorySegment.NULL)
}
public var flags: SimulationFlags
Which simulation stages to run for this source.
public var flags: SimulationFlags
get() = SimulationFlags(IPLSimulationInputs.flags(segment))
set(value) = IPLSimulationInputs.flags(segment, value.bits)
public var directFlags: DirectSimulationFlags
Which direct-path quantities to compute.
public var directFlags: DirectSimulationFlags
get() = DirectSimulationFlags(IPLSimulationInputs.directFlags(segment))
set(value) = IPLSimulationInputs.directFlags(segment, value.bits)
public var source: CoordinateSpace3
The source's position and orientation in world space.
public var source: CoordinateSpace3
get() = CoordinateSpace3.readFrom(IPLSimulationInputs.source(segment))
set(value) = value.writeTo(IPLSimulationInputs.source(segment))
public fun setSourcePosition(x: Float, y: Float, z: Float)
Sets just the source position, leaving orientation alone and allocating nothing.
public fun setSourcePosition(x: Float, y: Float, z: Float) {
val origin = IPLCoordinateSpace3.origin(IPLSimulationInputs.source(segment))
IPLVector3.x(origin, x)
IPLVector3.y(origin, y)
IPLVector3.z(origin, z)
}
public var occlusionType: OcclusionType
How occlusion is sampled.
public var occlusionType: OcclusionType
get() = OcclusionType.from(IPLSimulationInputs.occlusionType(segment))
set(value) = IPLSimulationInputs.occlusionType(segment, value.value)
public var occlusionRadius: Float
Radius of the source sphere, for OcclusionType.VOLUMETRIC.
public var occlusionRadius: Float
get() = IPLSimulationInputs.occlusionRadius(segment)
set(value) = IPLSimulationInputs.occlusionRadius(segment, value)
public var numOcclusionSamples: Int
Rays used for volumetric occlusion.
public var numOcclusionSamples: Int
get() = IPLSimulationInputs.numOcclusionSamples(segment)
set(value) = IPLSimulationInputs.numOcclusionSamples(segment, value)
public var numTransmissionRays: Int
Rays used to estimate transmission through geometry.
public var numTransmissionRays: Int
get() = IPLSimulationInputs.numTransmissionRays(segment)
set(value) = IPLSimulationInputs.numTransmissionRays(segment, value)
public fun setReverbScale(band: Int, scale: Float)
Per-band scaling applied to the simulated reverb time.
public fun setReverbScale(band: Int, scale: Float) {
if (band !in 0 until SteamAudio.NUM_BANDS) {
throw IndexOutOfBoundsException("band $band outside 0..<${SteamAudio.NUM_BANDS}")
}
IPLSimulationInputs.reverbScale(segment)
.setAtIndex(ValueLayout.JAVA_FLOAT, band.toLong(), scale)
}
public var hybridReverbTransitionTime: Float
Where the hybrid reflection type switches from convolution to parametric, in seconds.
Earlier means cheaper and less accurate. The early reflections carry the spatial impression of the room, so this wants to be long enough to include them -- a few hundred milliseconds in a large space.
public var hybridReverbTransitionTime: Float
get() = IPLSimulationInputs.hybridReverbTransitionTime(segment)
set(value) = IPLSimulationInputs.hybridReverbTransitionTime(segment, value)
public var hybridReverbOverlapPercent: Float
How much the convolution and parametric halves overlap, as a fraction.
public var hybridReverbOverlapPercent: Float
get() = IPLSimulationInputs.hybridReverbOverlapPercent(segment)
set(value) = IPLSimulationInputs.hybridReverbOverlapPercent(segment, value)
public var baked: Boolean
Whether to use baked data rather than simulating live.
public var baked: Boolean
get() = IPLSimulationInputs.baked(segment) != 0
set(value) = IPLSimulationInputs.baked(segment, if (value) 1 else 0)
public var enableValidation: Boolean
Extra validation of pathing results. Costs performance.
public var enableValidation: Boolean
get() = IPLSimulationInputs.enableValidation(segment) != 0
set(value) = IPLSimulationInputs.enableValidation(segment, if (value) 1 else 0)
public var findAlternatePaths: Boolean
Whether pathing should look for alternative routes when the direct path is blocked.
public var findAlternatePaths: Boolean
get() = IPLSimulationInputs.findAlternatePaths(segment) != 0
set(value) = IPLSimulationInputs.findAlternatePaths(segment, if (value) 1 else 0)
public var visRadius: Float
Probe visibility test radius, for pathing.
public var visRadius: Float
get() = IPLSimulationInputs.visRadius(segment)
set(value) = IPLSimulationInputs.visRadius(segment, value)
public var visThreshold: Float
Fraction of visibility rays that must pass for two probes to count as mutually visible.
public var visThreshold: Float
get() = IPLSimulationInputs.visThreshold(segment)
set(value) = IPLSimulationInputs.visThreshold(segment, value)
public var visRange: Float
Maximum distance over which probe visibility is tested.
public var visRange: Float
get() = IPLSimulationInputs.visRange(segment)
set(value) = IPLSimulationInputs.visRange(segment, value)
public var pathingOrder: Int
Ambisonic order used for pathing.
public var pathingOrder: Int
get() = IPLSimulationInputs.pathingOrder(segment)
set(value) = IPLSimulationInputs.pathingOrder(segment, value)
public fun setDistanceAttenuationModel(model: DistanceAttenuationModel)
The distance attenuation model to use for this source.
public fun setDistanceAttenuationModel(model: DistanceAttenuationModel) {
model.writeInto(IPLSimulationInputs.distanceAttenuationModel(segment))
}
public fun setAirAbsorptionModel(model: AirAbsorptionModel)
The air absorption model to use for this source.
public fun setAirAbsorptionModel(model: AirAbsorptionModel) {
model.writeInto(IPLSimulationInputs.airAbsorptionModel(segment))
}
public fun setDirectivity(directivity: Directivity)
The source's directivity pattern.
public fun setDirectivity(directivity: Directivity) {
directivity.writeInto(IPLSimulationInputs.directivity(segment))
}
public fun setPathingProbes(probeBatch: ProbeBatch?)
The probe batch to use for pathing, or null for none.
public fun setPathingProbes(probeBatch: ProbeBatch?) {
IPLSimulationInputs.pathingProbes(segment, probeBatch.handleOrNull())
}
public fun setBakedDataIdentifier(identifier: BakedDataIdentifier)
The baked data to look up when baked is set.
public fun setBakedDataIdentifier(identifier: BakedDataIdentifier) {
identifier.writeTo(IPLSimulationInputs.bakedDataIdentifier(segment))
}
}
public class SimulationOutputs private constructor
Simulation results for one source.
Read with Source.getOutputs; the fields are then handed to the matching effects. The direct
and reflection blocks are laid out exactly as IPLDirectEffectParams and
IPLReflectionEffectParams, so moving them across is a struct copy rather than a
field-by-field transcription -- see DirectEffectParams.copyFrom and
ReflectionEffectParams.copySimulatedFrom.
public class SimulationOutputs private constructor(
internal val segment: MemorySegment,
) {
public constructor(arena: Arena) : this(IPLSimulationOutputs.allocate(arena))
Allocates an outputs block from arena.
internal val directSegment: MemorySegment get() = IPLSimulationOutputs.direct(segment)
The direct-path result, shaped as IPLDirectEffectParams.
internal val reflectionsSegment: MemorySegment get() = IPLSimulationOutputs.reflections(segment)
The reflections result, shaped as IPLReflectionEffectParams.
internal val pathingSegment: MemorySegment get() = IPLSimulationOutputs.pathing(segment)
The pathing result, shaped as IPLPathEffectParams.
public fun copyDirectInto(params: DirectEffectParams)
Copies the simulated direct-path parameters into params.
public fun copyDirectInto(params: DirectEffectParams) {
params.segment.copyFrom(directSegment)
}
public val distanceAttenuation: Float
Simulated distance attenuation, as a linear gain.
public val distanceAttenuation: Float
get() = afield.audio.spatial.steamaudio.raw.IPLDirectEffectParams.distanceAttenuation(directSegment)
public val occlusion: Float
Simulated occlusion: 1 clear, 0 fully blocked.
public val occlusion: Float
get() = afield.audio.spatial.steamaudio.raw.IPLDirectEffectParams.occlusion(directSegment)
public fun airAbsorption(band: Int): Float
Simulated per-band air absorption gain.
public fun airAbsorption(band: Int): Float =
afield.audio.spatial.steamaudio.raw.IPLDirectEffectParams.airAbsorption(directSegment)
.getAtIndex(ValueLayout.JAVA_FLOAT, band.toLong())
public fun reverbTime(band: Int): Float
Simulated per-band RT60 reverb decay time, in seconds.
public fun reverbTime(band: Int): Float =
afield.audio.spatial.steamaudio.raw.IPLReflectionEffectParams.reverbTimes(reflectionsSegment)
.getAtIndex(ValueLayout.JAVA_FLOAT, band.toLong())
}
public class Simulator private constructor
Runs acoustic simulation: works out what happens to sound travelling from each source to the
listener through the Scene.
Threads -- read this before wiring it up
The simulator splits cleanly into two halves, and mixing them up is the single easiest way to ruin the audio:
| Where it runs | Cost | |
|---|---|---|
| runDirect | game thread or simulation thread | cheap; a few ray casts |
| runReflections | its own thread, never the audio thread | very expensive; milliseconds to tens of milliseconds |
| runPathing | its own thread | expensive |
| Source.getOutputs | anywhere | cheap; reads the last result |
| commit / Scene.commit | simulation thread, not concurrently with a run | expensive |
runReflections casts thousands of rays across several worker threads and blocks until done. Calling it from an audio callback will underrun. The intended arrangement is a dedicated simulation thread running at some low rate -- 10 to 30 Hz is plenty, since reverb changes slowly -- with the audio thread reading whatever the last completed result was:
// simulation thread, ~15 Hz:
simulator.setSharedInputs(flags, sharedInputs)
for (s in sources) s.setInputs(flags, s.inputs)
simulator.runReflections() // blocks here
// audio thread, every block:
source.getOutputs(SimulationFlags.REFLECTIONS, outputs) // cheap, non-blocking
reflectionParams.copySimulatedFrom(outputs)
reflectionEffect.apply(reflectionParams, dry, ambisonic)
Commit
commit applies pending source additions and removals. Like Scene.commit, it must not overlap a run against the same simulator.
public class Simulator private constructor(
handle: MemorySegment,
/** The settings this simulator was created with. */
public val settings: SimulationSettings,
) : SteamAudioObject(handle) {
override fun releaseAction(): (MemorySegment) -> Unit = Phonon::iplSimulatorRelease
override val typeName: String get() = "Simulator"
public fun setScene(scene: Scene)
Sets the scene to simulate against. Takes effect on the next commit.
The scene is not retained, so keep it alive for as long as the simulator uses it.
public fun setScene(scene: Scene) {
Phonon.iplSimulatorSetScene(liveHandle, scene.liveHandle)
}
public fun addProbeBatch(probeBatch: ProbeBatch)
Adds a probe batch, for baked reflections or pathing. Takes effect on the next commit.
public fun addProbeBatch(probeBatch: ProbeBatch) {
Phonon.iplSimulatorAddProbeBatch(liveHandle, probeBatch.liveHandle)
}
public fun removeProbeBatch(probeBatch: ProbeBatch)
Removes a probe batch. Takes effect on the next commit.
public fun removeProbeBatch(probeBatch: ProbeBatch) {
Phonon.iplSimulatorRemoveProbeBatch(liveHandle, probeBatch.liveHandle)
}
public fun setSharedInputs(flags: SimulationFlags, inputs: SimulationSharedInputs)
Publishes listener position and ray budget for the next run.
public fun setSharedInputs(flags: SimulationFlags, inputs: SimulationSharedInputs) {
Phonon.iplSimulatorSetSharedInputs(liveHandle, flags.bits, inputs.segment)
}
public fun commit()
Applies pending scene, source and probe-batch changes.
Must not run concurrently with runReflections or runPathing on the same simulator.
public fun commit() {
Phonon.iplSimulatorCommit(liveHandle)
}
public fun runDirect()
Runs direct-path simulation: occlusion, transmission, distance attenuation, directivity.
Cheap enough to run every frame on the game thread.
public fun runDirect() {
Phonon.iplSimulatorRunDirect(liveHandle)
}
public fun runReflections()
Runs reflection simulation. Blocking and expensive -- never call this from the audio thread.
See the threading table on this class.
public fun runReflections() {
Phonon.iplSimulatorRunReflections(liveHandle)
}
public fun runPathing()
Runs pathing simulation. Expensive; same threading rules as runReflections.
public fun runPathing() {
Phonon.iplSimulatorRunPathing(liveHandle)
}
public fun retain(): Simulator = Simulator(Phonon.iplSimulatorRetain(liveHandle), settings)
Increments the reference count and returns an independently closeable wrapper.
public companion object
public companion object {
public operator fun invoke
Creates a simulator.
public operator fun invoke(
context: SteamAudioContext,
settings: SimulationSettings,
openClDevice: OpenClDevice? = null,
radeonRaysDevice: RadeonRaysDevice? = null,
trueAudioNextDevice: TrueAudioNextDevice? = null,
): Simulator {
val contextHandle = context.liveHandle
Arena.ofConfined().use { arena ->
val native = settings.allocate(
arena,
openClDevice,
radeonRaysDevice,
trueAudioNextDevice,
)
return Simulator(
createHandle("iplSimulatorCreate") { out ->
Phonon.iplSimulatorCreate(contextHandle, native, out)
},
settings,
)
}
}
}
}
public class Source private constructor(handle: MemorySegment) : SteamAudioObject(handle)
One sound source within a Simulator.
A source is a simulation participant, not an audio stream: it carries position, directivity and occlusion settings in, and simulation results out. The audio itself flows through the effects you drive from those results.
public class Source private constructor(handle: MemorySegment) : SteamAudioObject(handle) {
override fun releaseAction(): (MemorySegment) -> Unit = Phonon::iplSourceRelease
override val typeName: String get() = "Source"
public fun addTo(simulator: Simulator)
Adds this source to simulator. Takes effect on the next Simulator.commit.
public fun addTo(simulator: Simulator) {
Phonon.iplSourceAdd(liveHandle, simulator.liveHandle)
}
public fun removeFrom(simulator: Simulator)
Removes this source from simulator. Takes effect on the next Simulator.commit.
public fun removeFrom(simulator: Simulator) {
Phonon.iplSourceRemove(liveHandle, simulator.liveHandle)
}
public fun setInputs(flags: SimulationFlags, inputs: SimulationInputs)
Publishes this source's inputs for the next simulation run.
public fun setInputs(flags: SimulationFlags, inputs: SimulationInputs) {
Phonon.iplSourceSetInputs(liveHandle, flags.bits, inputs.segment)
}
public fun getOutputs(flags: SimulationFlags, outputs: SimulationOutputs)
Reads the most recent simulation results into outputs.
Cheap and non-blocking -- it copies whatever the last completed run produced -- so this is safe to call from the audio thread every block.
public fun getOutputs(flags: SimulationFlags, outputs: SimulationOutputs) {
Phonon.iplSourceGetOutputs(liveHandle, flags.bits, outputs.segment)
}
public fun retain(): Source = Source(Phonon.iplSourceRetain(liveHandle))
Increments the reference count and returns an independently closeable wrapper.
public companion object
public companion object {
public operator fun invoke
Creates a source in simulator.
@param flags which simulation stages this source takes part in. Must be a subset of the simulator's own flags.
public operator fun invoke(
simulator: Simulator,
flags: SimulationFlags = SimulationFlags.DIRECT or SimulationFlags.REFLECTIONS,
): Source {
val simulatorHandle = simulator.liveHandle
Arena.ofConfined().use { arena ->
val settings = IPLSourceSettings.allocate(arena)
IPLSourceSettings.flags(settings, flags.bits)
return Source(
createHandle("iplSourceCreate") { out ->
Phonon.iplSourceCreate(simulatorHandle, settings, out)
},
)
}
}
}
}