Skip to main content

Footsteps in snow

Template by nthState

Space · PHASE · 1 source · 0:30 preview

Preview

Binaural preview · wear headphones. The app rendered it offline through AVAudioEnvironmentNode: it is a preview, not PHASE’s own output.

Download Footsteps in snow.phonique

A .phonique document is a folder, so it comes as a ZIP archive, 2.2 MB. Unzip it and open the folder in Phonique.

Opening a template straight from this page waits until the app can open one; until then the download is the way in.

What’s inside

1 source, 0 occluders and a room, read from the template’s own document.

Sources

  • Footsteps8 files · Playback: recur

The room

Reverb: none · 1 ambient bed

The Swift

What Phonique exports from this template: a Swift package, FootstepsInSnow, that needs nothing beyond Apple’s frameworks. Its README’s “Use it”:

import FootstepsInSnow
let space = try FootstepsInSnowSpace()
try space.start()
space.pause()
try space.start() // resumes
space.stop()
The whole of FootstepsInSnowSpace.swift, 373 lines
//
//  FootstepsInSnowSpace.swift
//  Generated by Phonique from “Footsteps in snow”.
//
//  Phonique writes this file afresh each time it exports the graph, so a
//  change made here is gone after the next export.
//

import AVFAudio
import Foundation
import ModelIO
import PHASE
import simd

/// The document's Space. Make a fresh instance after stop(); pause/start resumes it.
/// PHASE renders in real time. The caller owns its audio session and calls stop() when done.
@MainActor
public final class FootstepsInSnowSpace {
    public enum Failure: Error { case missingAsset(String), invalidLayout, soundFailed(Int) }
    public enum State { case built, playing, paused, stopped }
    public let engine: PHASEEngine
    public let listener: PHASEListener
    public private(set) var state: State = .built
    public private(set) var sources: [String: PHASESource] = [:]
    public private(set) var occluders: [String: PHASEOccluder] = [:]
    /// Names for comparing the built scene through PHASE's public API.
    public private(set) var namedObjects: [(object: AnyObject, name: String)] = []
    public private(set) var registeredAssets: [String] = []
    public private(set) var failure: (any Error)?
    private var sounds: [Sound] = []
    private var events: [UUID: PHASESoundEvent] = [:]
    private var motion: [String: (Double) -> simd_float4x4] = [:]
    private var mover: Task<Void, Never>?
    private var origin: Int64?
    private var heldSeconds = 0.0

    /// Builds without starting. listenerGain: 0 permits silent structural tests.
    public init(listenerGain: Double = 1) throws {
        engine = PHASEEngine(updateMode: .automatic)
        engine.outputSpatializationMode = .automatic
        engine.defaultReverbPreset = .none
        engine.defaultMedium = PHASEMedium(engine: engine, preset: .air)
        listener = PHASEListener(engine: engine)
        listener.transform = simd_float4x4(columns: (SIMD4<Float>(1.0, 0.0, 0.0, 0.0), SIMD4<Float>(0.0, 1.0, 0.0, 0.0), SIMD4<Float>(0.0, 0.0, 1.0, 0.0), SIMD4<Float>(0.0, 0.0, 0.0, 1.0)))
        listener.gain = listenerGain
        do {
            try engine.rootObject.addChild(listener)
            namedObjects.append((listener, "listener"))
            let assets = try Self.audioFiles()
            try register("audio/step-1.wav.mono", files: assets)
            try register("audio/step-2.wav.mono", files: assets)
            try register("audio/step-3.wav.mono", files: assets)
            try register("audio/step-4.wav.mono", files: assets)
            try register("audio/step-5.wav.mono", files: assets)
            try register("audio/step-6.wav.mono", files: assets)
            try register("audio/step-7.wav.mono", files: assets)
            try register("audio/step-8.wav.mono", files: assets)
            try register("audio/wind.wav", files: assets)
            do {
                let object = PHASESource(engine: engine)
                object.transform = simd_float4x4(columns: (SIMD4<Float>(1.0, 0.0, 0.0, 0.0), SIMD4<Float>(0.0, 1.0, 0.0, 0.0), SIMD4<Float>(0.0, 0.0, 1.0, 0.0), SIMD4<Float>(-3.0, -1.5, -2.0, 1.0)))
                object.gain = 1.0
                try engine.rootObject.addChild(object)
                sources["7A3C6F1E-529B-4D83-9A47-6D128E5BC401"] = object
                namedObjects.append((object, "7A3C6F1E-529B-4D83-9A47-6D128E5BC401"))
                let parameters = PHASEMixerParameters()
                parameters.addSpatialMixerParameters(identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer", source: object, listener: listener)
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-1.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.0")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.0")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.0")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-2.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.1")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.1")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.1")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-3.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.2")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.2")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.2")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-4.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.3")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.3")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.3")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-5.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.4")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.4")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.4")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-6.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.5")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.5")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.5")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-7.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.6")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.6")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.6")
                }
                do {
                    guard let pipeline = PHASESpatialPipeline(flags: [.directPathTransmission, .earlyReflections, .lateReverb]) else { throw Failure.invalidLayout }
                    pipeline.entries[.directPathTransmission]?.sendLevel = 1.0
                    pipeline.entries[.directPathTransmission]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.directPath")
                    pipeline.entries[.earlyReflections]?.sendLevel = 1.0
                    pipeline.entries[.earlyReflections]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.earlyReflections")
                    pipeline.entries[.lateReverb]?.sendLevel = 1.0
                    pipeline.entries[.lateReverb]?.sendLevelMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.send.lateReverb")
                    let mixer = PHASESpatialMixerDefinition(spatialPipeline: pipeline, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.mixer")
                    let distance = PHASEGeometricSpreadingDistanceModelParameters()
                    distance.rolloffFactor = 1.0
                    distance.fadeOutParameters = PHASEDistanceModelFadeOutParameters(cullDistance: 12.0)
                    mixer.distanceModelParameters = distance
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/step-8.wav.mono", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.sampler.7")
                    sampler.playbackMode = .oneShot
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.7")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.7")
                }
                sounds.append(Sound(owner: "7A3C6F1E-529B-4D83-9A47-6D128E5BC401", assets: ["7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.0", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.1", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.2", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.3", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.4", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.5", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.6", "7A3C6F1E-529B-4D83-9A47-6D128E5BC401.event.7"], parameters: parameters, isOn: true, interval: 0.6...0.8))
                motion["7A3C6F1E-529B-4D83-9A47-6D128E5BC401"] = { seconds in Self.transform(base: simd_float4x4(columns: (SIMD4<Float>(1.0, 0.0, 0.0, 0.0), SIMD4<Float>(0.0, 1.0, 0.0, 0.0), SIMD4<Float>(0.0, 0.0, 1.0, 0.0), SIMD4<Float>(-3.0, -1.5, -2.0, 1.0))), route: [SIMD3<Double>(-3.0, -1.5, -2.0), SIMD3<Double>(3.0, -1.5, -2.0)], duration: 6.0, mode: "thereAndBack", seconds: seconds) }
            }
            do {
                let parameters = PHASEMixerParameters()
                parameters.addAmbientMixerParameters(identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421.mixer", listener: listener)
                do {
                    guard let layout = layouts["audio/wind.wav"] else { throw Failure.invalidLayout }
                    let mixer = PHASEAmbientMixerDefinition(channelLayout: layout, orientation: simd_quatf(vector: SIMD4<Float>(0.0, 0.0, 0.0, 1.0)), identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421.mixer")
                    mixer.gain = 1.0
                    mixer.gainMetaParameterDefinition = PHASENumberMetaParameterDefinition(value: 1.0, minimum: 0.0, maximum: 1.0, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421.gain")
                    let sampler = PHASESamplerNodeDefinition(soundAssetIdentifier: "audio/wind.wav", mixerDefinition: mixer, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421.sampler.0")
                    sampler.playbackMode = .looping
                    _ = try engine.assetRegistry.registerSoundEventAsset(rootNode: sampler, identifier: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421.event.0")
                    registeredAssets.append("7A3C6F1E-529B-4D83-9A47-6D128E5BC421.event.0")
                }
                sounds.append(Sound(owner: "7A3C6F1E-529B-4D83-9A47-6D128E5BC421", assets: ["7A3C6F1E-529B-4D83-9A47-6D128E5BC421.event.0"], parameters: parameters, isOn: true, interval: nil))
            }
            for sound in sounds where sound.isOn && sound.interval == nil { try play(sound, starting: false) }
        } catch {
            stop()
            throw error
        }
    }

    private final class Sound {
        let owner: String
        let assets: [String]
        let parameters: PHASEMixerParameters
        let isOn: Bool
        let interval: ClosedRange<Double>?
        var schedule: Task<Void, Never>?
        init(owner: String, assets: [String], parameters: PHASEMixerParameters, isOn: Bool, interval: ClosedRange<Double>?) {
            self.owner = owner; self.assets = assets; self.parameters = parameters; self.isOn = isOn; self.interval = interval
        }
    }
    public var renderSeconds: Double {
        guard state == .playing, let time = engine.lastRenderTime, time.isSampleTimeValid, time.sampleRate > 0 else { return heldSeconds }
        guard let origin else { self.origin = time.sampleTime; return heldSeconds }
        // PHASE can reset its sample clock after a pause. Establish the new origin
        // rather than moving a path backwards or waiting for the old clock to catch up.
        if time.sampleTime < origin { self.origin = time.sampleTime; return heldSeconds }
        return heldSeconds + Double(time.sampleTime - origin) / time.sampleRate
    }
    public func start() throws {
        guard state == .built || state == .paused else { return }
        let first = state == .built
        try engine.start()
        state = .playing
        moveSources(at: renderSeconds)
        if first { for (token, event) in events { event.start(completion: Self.ended(self, token: token)) } }
        for sound in sounds where sound.isOn {
            guard let interval = sound.interval else { continue }
            sound.schedule = Task { @MainActor [weak self, weak sound] in
                let clock = ContinuousClock()
                var due = clock.now
                while !Task.isCancelled {
                    due = due.advanced(by: .seconds(Double.random(in: interval)))
                    do { try await Task.sleep(until: due, clock: clock) } catch { return }
                    guard !Task.isCancelled else { return }
                    do {
                        guard let self, let sound, self.state == .playing else { return }
                        self.moveSources(at: self.renderSeconds)
                        try self.play(sound, starting: true)
                    } catch { self?.failure = error; return }
                }
            }
        }
        if !motion.isEmpty {
            mover = Task { @MainActor [weak self] in
                while !Task.isCancelled {
                    guard self?.state == .playing else { return }
                    if let seconds = self?.renderSeconds { self?.moveSources(at: seconds) }
                    do { try await Task.sleep(for: .seconds(0.01)) } catch { return }
                }
            }
        }
    }
    public func pause() {
        guard state == .playing else { return }
        heldSeconds = renderSeconds; origin = nil
        engine.pause(); state = .paused
        mover?.cancel(); mover = nil
        for sound in sounds { sound.schedule?.cancel(); sound.schedule = nil }
    }
    public func stop() {
        guard state != .stopped else { return }
        heldSeconds = renderSeconds; origin = nil
        mover?.cancel(); mover = nil
        for sound in sounds { sound.schedule?.cancel(); sound.schedule = nil }
        for event in events.values { event.stopAndInvalidate() }
        events = [:]
        engine.stop()
        for asset in registeredAssets.reversed() { engine.assetRegistry.unregisterAsset(identifier: asset, completion: Self.unregistered()) }
        registeredAssets = []
        for source in sources.values { engine.rootObject.removeChild(source) }
        for occluder in occluders.values { engine.rootObject.removeChild(occluder) }
        engine.rootObject.removeChild(listener)
        sounds = []; sources = [:]; occluders = [:]; motion = [:]; namedObjects = []
        state = .stopped
    }
    public func moveSources(at seconds: Double) {
        for (id, transform) in motion { sources[id]?.transform = transform(seconds) }
    }
    private func play(_ sound: Sound, starting: Bool) throws {
        guard let identifier = sound.assets.randomElement() else { return }
        let event = try PHASESoundEvent(engine: engine, assetIdentifier: identifier, mixerParameters: sound.parameters)
        let token = UUID()
        events[token] = event
        namedObjects.append((event, sound.owner + ".event"))
        if starting { event.start(completion: Self.ended(self, token: token)) }
    }
    private nonisolated static func ended(_ space: FootstepsInSnowSpace, token: UUID) -> @Sendable (PHASESoundEvent.StartHandlerReason) -> Void {
        { [weak space] reason in
            let raw = reason.rawValue
            Task { @MainActor in
                guard let space, let event = space.events.removeValue(forKey: token) else { return }
                space.namedObjects.removeAll { $0.object === event }
                if raw == PHASESoundEvent.StartHandlerReason.failure.rawValue { space.failure = Failure.soundFailed(raw) }
            }
        }
    }
    private nonisolated static func unregistered() -> @Sendable (Bool) -> Void { { _ in } }
    private nonisolated static func transform(base: simd_float4x4, route: [SIMD3<Double>], duration: Double, mode: String, seconds: Double) -> simd_float4x4 {
        guard let start = route.first, seconds.isFinite, seconds > 0 else { return base }
        let legs = zip(route, route.dropFirst()).map { simd_distance($0, $1) }
        let total = legs.reduce(0, +)
        guard total > 0 else { return base }
        let laps = seconds / duration
        let fraction = mode == "loop" ? laps - laps.rounded(.down) : mode == "thereAndBack" ? 1 - abs(laps.truncatingRemainder(dividingBy: 2) - 1) : min(laps, 1)
        var left = fraction * total
        var position = start
        for (index, leg) in legs.enumerated() {
            if left <= leg || index == legs.count - 1 {
                let share = leg > 0 ? min(1, max(0, left / leg)) : 0
                position = route[index] + (route[index + 1] - route[index]) * share
                break
            }
            left -= leg
        }
        var result = base
        result.columns.3 = SIMD4<Float>(Float(position.x), Float(position.y), Float(position.z), 1)
        return result
    }

    private var layouts: [String: AVAudioChannelLayout] = [:]
    private static func audioFiles() throws -> [String: String] {
        guard let url = Bundle.module.url(forResource: "FootstepsInSnowSpace.assets", withExtension: "json") else { throw Failure.missingAsset("FootstepsInSnowSpace.assets.json") }
        return try JSONDecoder().decode([String: String].self, from: Data(contentsOf: url))
    }
    private func register(_ identifier: String, files: [String: String]) throws {
        guard let file = files[identifier], let url = Bundle.module.url(forResource: file, withExtension: nil) else { throw Failure.missingAsset(identifier) }
        let audio = try AVAudioFile(forReading: url)
        let count = audio.processingFormat.channelCount
        let tag = count == 1 ? kAudioChannelLayoutTag_Mono : count == 2 ? kAudioChannelLayoutTag_Stereo : kAudioChannelLayoutTag_DiscreteInOrder | count
        guard let layout = AVAudioChannelLayout(layoutTag: tag) else { throw Failure.invalidLayout }
        _ = try engine.assetRegistry.registerSoundAsset(url: url, identifier: identifier, assetType: .resident, channelLayout: layout, normalizationMode: .none)
        layouts[identifier] = layout
        registeredAssets.append(identifier)
    }
}

Tags

  • Space · PHASE

Licence

Free to use in your own apps, commercial or not, with no credit needed. The terms of use carry the licence in full.