Skip to content

AudioReturnValue.kt

src/jvmMain/kotlin/afield/audio/synth/AudioReturnValue.kt · 133 lines

@file:afield.runtime.completion.CompletionCategory("Synthesis")

The step between a box whose last expression is a patch and a patch you can hear.

audio { … } builds an AudioPatchSpec and stops there, holding no device and making no sound. This file is the other half: a return-value handler that recognises an AudioPatchSpec, a bare Signal or a StereoRenderable coming back from a box, registers it with FieldAudioEngine so it joins the mix, and unregisters it when the box stops. The two bare forms are wrapped with AudioPatchSpec.from first, so returning an oscillator and returning a patch built around one reach the same place.

What the handler launches is a coroutine that mostly yields. Being cancellable is the work: it is registered as one of the box's jobs, it re-checks the box's run generation on every pass, and it closes the engine handle in a finally. A re-run therefore silences the previous patch instead of stacking a second copy of it on top.

The same loop redraws the box's inline output. When box execution has marked the returned value's line as an inline summary target, the loop pushes the patch's summaryHtml, a waveform and a spectrum drawn from its own rolling trace, back to the editor twelve times a second, beginning 120 ms after the launch.

package and imports — 51 lines, from line 23
package afield.audio.synth

import afield.ai.agent.*
import afield.ai.conversation.*
import afield.ai.tools.*
import afield.animation.gesture.*
import afield.animation.interpolation.*
import afield.animation.keyframes.*
import afield.app.diagnostics.*
import afield.app.notifications.*
import afield.app.protocol.*
import afield.app.server.*
import afield.app.settings.*
import afield.audio.synth.*
import afield.data.table.*
import afield.document.context.*
import afield.document.graph.*
import afield.document.history.*
import afield.document.persistence.*
import afield.document.properties.*
import afield.document.workspace.*
import afield.graphics.color.*
import afield.graphics.paint.*
import afield.graphics.vulkan.*
import afield.math.functions.*
import afield.media.capture.*
import afield.media.stream.*
import afield.media.video.*
import afield.runtime.completion.*
import afield.runtime.execution.*
import afield.runtime.kotlin.*
import afield.runtime.lifecycle.*
import afield.tools.automation.*
import afield.tools.docs.*
import afield.ui.controls.*
import afield.ui.layout.*
import afield.ui.notation.*
import afield.ui.output.*
import afield.ui.output.templates.*
import afield.ui.slides.*
import afield.ui.visualization.*
import kotlinx.coroutines.yield

private const val INLINE_AUDIO_SUMMARY_REFRESH_HZ = 12.0
private const val INLINE_AUDIO_SUMMARY_INITIAL_DELAY_MS = 120L

private fun Box.pushInlineExecutionOutput(output: Output) {
    val normalized = if (output.uid.isBlank()) output.copy(uid = id) else output
    val message = Message("output").payload(normalized)
    inside.sessions.forEach { _send(it, message) }
}

fun registerAudioReturnValue()

Teaches box execution about AudioPatchSpec, Signal and StereoRenderable. Called once during return-value registration, not from box code.

Handlers are registered in priority order and tried in that order, each returning false to pass the value on. This one sits at 10, behind the Sequence and Frames handlers in afield and ahead of the pattern responder.

body — 50 lines, from line 83
@RunsAt(returnValueRegistration, priority = 10)
fun registerAudioReturnValue() {
    ReturnValueRegistry.handlers += handler@{ inside, returnValue, strand, generation, launchEpoch, notify ->
        val spec = when (returnValue) {
            is AudioPatchSpec -> returnValue
            is Signal -> AudioPatchSpec.from(returnValue)
            is StereoRenderable -> AudioPatchSpec.from(returnValue)
            else -> return@handler false
        }

        val handle = FieldAudioEngine.shared().register(spec)

        inMain {
            val job = kotlin.coroutines.coroutineContext[kotlinx.coroutines.Job]!!
            if (!inside.registerLaunchJob(job, launchEpoch)) {
                handle.close()
                notify(false, strand, generation)
                return@inMain
            }
            notify(true, strand, generation)
            val inlineTarget = spec.inlineSummaryTarget
            val refreshIntervalNs = (1_000_000_000.0 / INLINE_AUDIO_SUMMARY_REFRESH_HZ).toLong().coerceAtLeast(1L)
            var nextInlineRefreshAt = System.nanoTime() + INLINE_AUDIO_SUMMARY_INITIAL_DELAY_MS * 1_000_000L
            try {
                while (inside.runGeneration[strand] == generation) {
                    if (inlineTarget != null && System.nanoTime() >= nextInlineRefreshAt) {
                        inside.pushInlineExecutionOutput(
                            Output(
                                uid = inside.id,
                                line = inlineTarget.line,
                                kind = "normal",
                                property = inlineTarget.property,
                                html = spec.summaryHtml(),
                                annotation = inlineTarget.annotation ?: "",
                                frame = Runloop.runLoop.frame.toInt()
                            )
                        )
                        nextInlineRefreshAt = System.nanoTime() + refreshIntervalNs
                    }
                    yield()
                }
            } finally {
                handle.close()
                inside.jobs.remove(job)
                notify(false, strand, generation)
            }
        }
        true
    }
}