Skip to content

KotlinJsToneSurfaceSmokeTest.kt

src/jvmMain/kotlin/afield/examples/KotlinJsToneSurfaceSmokeTest.kt · 180 lines

package afield.examples

import afield.audio.tone.ToneKotlinJs
import afield.lang.remotejs.KotlinJsHeadProfiles
import afield.lang.remotejs.KotlinToJsCompiler

FIELD_TEST id: remotejs.kotlinjs-tone-surface runner: jvm-main entrypoint: afield.examples.KotlinJsToneSurfaceSmokeTest tags: [smoke, remotejs, kotlinjs, tone] timeoutSec: 120 expect: exitCode: 0 stdoutContains: - "KotlinJsToneSurfaceSmokeTest ok"

object KotlinJsToneSurfaceSmokeTest

Validates the Tone.js computer-music head profile (// {{ tone }}) by compiling a sample that exercises a broad slice of the surface against the GENERATED compiler external declarations — the real proof the ToneKotlinJs binding emits valid Kotlin/JS for every bound name and that they type-check together. Also asserts the marker routes to the tone profile and the parity/dynamic split invariants (afield.graphics.KotlinJsBinding) hold for the Tone surface.

Grounding: every name here is confirmed present in the vendored bundle extensions/remote_ar/vendor/tone/15.1.22/Tone.js (which the RemoteAR head installs as Tone). Audio playback requires a real browser head + a user gesture and is NOT verified here.

object KotlinJsToneSurfaceSmokeTest {

fun main(args: Array<String>)

body — 128 lines, from line 33
    @JvmStatic
    fun main(args: Array<String>) {
        val compiler = ToneKotlinJs.declarations
        val completion = ToneKotlinJs.completionDeclarations

        // Structural parity: compiler is `external`, completion is plain.
        require(compiler.contains("external object Tone")) { "compiler Tone must be external" }
        require(completion.contains("object Tone") && !completion.contains("external")) {
            "completion mirror must be plain (no `external`)"
        }

        // Core classes present in BOTH renderings.
        for (t in listOf("Synth", "PolySynth", "MonoSynth", "Oscillator", "Gain", "Filter", "Reverb",
            "FeedbackDelay", "Loop", "Sequence", "Transport", "Param", "Destination")) {
            require(compiler.contains("class $t")) { "compiler missing Tone.$t" }
            require(completion.contains("class $t")) { "completion missing Tone.$t" }
        }

        // Top-level Tone.* helpers exist as member funs in both façades.
        for (fn in listOf("fun now(", "fun start(", "fun getTransport(", "fun getDestination(",
            "fun dbToGain(", "fun mtof(")) {
            require(compiler.contains(fn)) { "compiler missing Tone helper `$fn`" }
            require(completion.contains(fn)) { "completion missing Tone helper `$fn`" }
        }

        // dynamic ↔ Any? split: Transport.position is a mutable dynamic for the compiler, typed for completion.
        require(compiler.contains("var position: dynamic")) { "Transport.position must be `dynamic` for the compiler" }
        require(completion.contains("var position: Any?")) { "Transport.position must be `Any?` for completion" }
        // Sequence's callback carries a dynamic value in the compiler, Any? in completion.
        require(compiler.contains("value: dynamic")) { "Sequence callback value must be `dynamic` for the compiler" }
        require(completion.contains("value: Any?")) { "Sequence callback value must be `Any?` for completion" }

        // Runtime-shape grounding: Tone's global Transport/Destination are singleton values, its
        // scheduling primitives are ToneEvents (not connectable audio nodes), and percussive synths
        // do not take pitched-note trigger signatures.
        require(compiler.contains("class Transport private constructor()")) { "Transport must not be constructible" }
        require(compiler.contains("class Destination private constructor()")) { "Destination must not be constructible" }
        require(completion.contains("class Loop") && completion.contains(": ToneEvent()")) {
            "Loop/Sequence/Part must use the ToneEvent shape"
        }
        val metalBlock = compiler.substringAfter("class MetalSynth").substringBefore("class PluckSynth")
        require("duration: String" in metalBlock && "note: String" !in metalBlock) {
            "MetalSynth must expose percussive, not pitched-note, triggers"
        }
        val gainBlock = compiler.substringAfter("class Gain").substringBefore("class Volume")
        require("fun sync(" !in gainBlock && "fun unsync(" !in gainBlock) {
            "ordinary effects must not inherit nonexistent sync/unsync methods"
        }

        // Compiler-only glue must not leak into completion.
        require(compiler.contains("__toneLog")) { "compiler missing log glue" }
        require(!completion.contains("__toneLog")) { "completion must not carry compiler-only glue" }

        // The whole surface must type-check against the generated compiler declarations.
        val sample = """
            // {{ tone }}
            val synth = Tone.PolySynth()
            synth.toDestination()
            val reverb = Tone.Reverb(2.0)
            reverb.toDestination()
            synth.connect(reverb)
            synth.volume.value = -6.0
            Tone.start()
            val transport = Tone.getTransport()
            transport.bpm.value = 120.0
            transport.swing = 0.1
            transport.position = "0:0:0"
            val notes = arrayOf("C4", "E4", "G4")
            val repeatId = transport.scheduleRepeat({ time -> synth.triggerAttackRelease(notes, "8n", time) }, "2n")
            transport.clear(repeatId)
            val loop = Tone.Loop({ time -> synth.triggerAttackRelease("C2", "16n", time) }, "4n")
            loop.start(0.0).stop(8.0)
            val seq = Tone.Sequence({ time, note -> log("step " + note) }, arrayOf<Any?>("C4", "E4"), "8n")
            seq.start()
            val osc = Tone.Oscillator(220.0, "sawtooth")
            osc.frequency.value = 330.0
            osc.detune.value = 5.0
            osc.start()
            val filter = Tone.Filter(800.0, "lowpass")
            filter.Q.value = 4.0
            val delay = Tone.FeedbackDelay(0.25, 0.4)
            osc.chain(filter, delay, Tone.getDestination())
            val mono = Tone.MonoSynth()
            mono.toDestination()
            mono.triggerAttackRelease("A3", "4n", Tone.now())
            val gain = Tone.Gain(0.8)
            gain.gain.rampTo(0.2, 0.5)
            val noise = Tone.Noise("pink")
            noise.type = "white"
            Tone.getDestination().volume.value = -3.0
            transport.start()
            log("tone surface ok " + Tone.version + " " + Tone.now() + " " + Tone.dbToGain(-6.0) +
                " " + Tone.mtof(60.0) + " " + osc.type + " " + filter.frequency.value + " " +
                delay.feedback.value + " " + reverb.decay + " " + transport.state + " " + gain.gain.value)
        """.trimIndent()

        val resolved = KotlinJsHeadProfiles.resolve(sample)
        require(resolved.profile === KotlinJsHeadProfiles.tone) { "sample must select the tone head" }
        val prepared = resolved.profile.prepareSource(resolved.source)
        val result = KotlinToJsCompiler.compile(prepared, moduleName = "tone_surface", wrapImplicitMain = false)
        require(result.success && result.js != null) {
            "Tone surface did not compile:\n${result.rawLog.takeLast(2000)}"
        }
        println("tone surface compiled: ${prepared.length} chars Kotlin -> ${result.js.length} chars JS")

        // Live completion (skip-tolerant — needs the AA sidecar; feeds `completionDeclarations`).
        System.getProperty("afield.completion.sidecarUrl")
            ?: System.getenv("AFIELD_CODE_USE_SEMANTIC_SIDECAR_URL")?.let { System.setProperty("afield.completion.sidecarUrl", it) }
        val toneDot = complete("Tone.")
        if (toneDot == null) {
            println("no AA sidecar reachable; façade completion checks skipped")
        } else {
            require(toneDot.any { it.startsWith("PolySynth") } && toneDot.any { it.startsWith("getTransport") }) {
                "Tone. completion missing PolySynth/getTransport: ${toneDot.take(25)}"
            }
            val synthDot = complete("Tone.PolySynth().") ?: emptyList()
            require(synthDot.any { it.startsWith("triggerAttackRelease") } && synthDot.any { it.startsWith("toDestination") }) {
                "Tone.PolySynth(). completion missing triggerAttackRelease/toDestination: ${synthDot.take(25)}"
            }
            val txDot = complete("Tone.getTransport().") ?: emptyList()
            require(txDot.any { it.startsWith("scheduleRepeat") } && txDot.any { it.startsWith("bpm") }) {
                "Tone.getTransport(). completion missing scheduleRepeat/bpm: ${txDot.take(25)}"
            }
            println("completion: Tone. → PolySynth/getTransport present; PolySynth(). → triggerAttackRelease present; getTransport(). → scheduleRepeat present")
        }

        println("KotlinJsToneSurfaceSmokeTest ok")
    }

Complete against the generated completion mirror; null if no sidecar answered (→ skip).

    private fun complete(snippet: String): List<String>? {
        val decl = ToneKotlinJs.completionDeclarations
        val roots = afield.runtime.completion.AaCompletionClient.kotlinStdlibRoots()
        var unreachable = true
        repeat(8) {
            val o = afield.runtime.completion.AaCompletionClient.complete(
                snippet = snippet, cursor = snippet.length, preamble = "",
                declarations = decl, implicitReceivers = emptyList(), classpathRootsOverride = roots,
                jsTarget = true,
            )
            if (o.detail.let { !it.startsWith("no-sidecar") && !it.startsWith("error") && !it.startsWith("http") }) unreachable = false
            (o.result as? afield.runtime.kotlin.CompletionResult.Success)?.let { return it.metadata.map { m -> m.text } }
            Thread.sleep(400)
        }
        return if (unreachable) null else emptyList()
    }
}