diff --git a/src/jsAudioWorkletMain/kotlin/nl/astraeus/processor/WorkletProcessor.kt b/src/jsAudioWorkletMain/kotlin/nl/astraeus/processor/WorkletProcessor.kt new file mode 100644 index 0000000..85a3c7d --- /dev/null +++ b/src/jsAudioWorkletMain/kotlin/nl/astraeus/processor/WorkletProcessor.kt @@ -0,0 +1,46 @@ +@file:OptIn(ExperimentalJsExport::class) + +package nl.astraeus.processor + +import org.khronos.webgl.Float64Array +import org.w3c.dom.MessageEvent +import org.w3c.dom.MessagePort + +@ExperimentalJsExport +@JsExport +object WorkletProcessor { + var port: MessagePort? = null + + @JsName("setPort") + fun setPort(port: MessagePort) { + this.port = port + this.port?.onmessage = ::onMessage + } + + @JsName("onMessage") + fun onMessage(message: MessageEvent) { + console.log("WorkletProcessor: Received message", message) + + when (message.data) { + "start" -> { + println("Start worklet!") + } + "stop" -> { + + } + else -> + console.error("Don't kow how to handle message", message) + } + } + + @JsName("process") + fun process(samples: Int, left: Float64Array, right: Float64Array) { + //console.log("WorkletProcessor.process", samples) +// val buffer = Float64SampleBuffer(samples, left, right) +// +// audioGenerator?.also { generator -> +// generator.fillBuffer(buffer, 0, samples, false) +// } + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/Main.kt b/src/jsMain/kotlin/nl/astraeus/Main.kt new file mode 100644 index 0000000..f9741eb --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/Main.kt @@ -0,0 +1,21 @@ +package nl.astraeus + +import kotlinx.browser.document +import nl.astraeus.handler.AudioWorkletHandler + +fun main() { + AudioWorkletHandler.loadCode() + + println("Ok") + + document.getElementById("clicker")?.also { + it.addEventListener("click", { + AudioWorkletHandler.createContext { + println("Created context") + + AudioWorkletHandler.start() + } + }, "") + } + +} diff --git a/src/jsMain/kotlin/nl/astraeus/handler/AudioWorkletHandler.kt b/src/jsMain/kotlin/nl/astraeus/handler/AudioWorkletHandler.kt new file mode 100644 index 0000000..af5c0f5 --- /dev/null +++ b/src/jsMain/kotlin/nl/astraeus/handler/AudioWorkletHandler.kt @@ -0,0 +1,162 @@ +package nl.astraeus.handler + +import kotlinx.browser.window +import org.w3c.dom.MessageEvent +import org.w3c.dom.MessagePort +import org.w3c.dom.url.URL +import org.w3c.files.Blob +import org.w3c.files.FilePropertyBag + +enum class WorkletState { + INIT, + LOADING, + LOADED, + READY +} + +abstract class AudioWorklet( + val jsCodeFile: String, + val kotlinCodeFile: String +) { + var audioContext: dynamic = null + var state = WorkletState.INIT + var processingCode: Blob? = null + var sampleRate: Int = 44100 + var audioWorkletMessagePort: MessagePort? = null + + abstract fun createNode(audioContext: dynamic): dynamic + + abstract fun onAudioWorkletMessage(message: MessageEvent) + + abstract fun onCodeLoaded() + + fun loadCode() { + // hack + // concat kotlin js and note-processor.js because + // audio worklet es6 is not supported in kotlin yet + state = WorkletState.LOADING + + window.fetch(jsCodeFile).then { daResponse -> + if (daResponse.ok) { + daResponse.text().then { daText -> + window.fetch(kotlinCodeFile).then { npResponse -> + if (npResponse.ok) { + npResponse.text().then { npText -> + processingCode = Blob( + arrayOf(daText, npText), + FilePropertyBag(type = "application/javascript") + ) + + state = WorkletState.LOADED + + println("Loaded $this code") + + onCodeLoaded() + } + } + } + } + } + } + } + + fun createContext(callback: () -> Unit) { + js("window.AudioContext = window.AudioContext || window.webkitAudioContext") + + audioContext = js("new window.AudioContext()") + sampleRate = audioContext.sampleRate as Int + + check(state == WorkletState.LOADED) { + "Can not createContext when code is not yet loaded, call loadCode first" + } + + val module = audioContext.audioWorklet.addModule( + URL.createObjectURL(processingCode!!) + ) + + module.then { + val node: dynamic = createNode(audioContext) + + node.connect(audioContext.destination) + + node.port.onmessage = ::onAudioWorkletMessage + + audioWorkletMessagePort = node.port as? MessagePort + + state = WorkletState.READY + + //postBatchedRequests() + + callback() + + "dynamic" + } + } + + fun isResumed(): Boolean = audioContext?.state == "running" + + fun resume() { + check(state == WorkletState.READY) { + "Unable to resume, state is not READY [$state]" + } + + audioContext?.resume() + } + +/* fun postRequest(request: WorkerRequest) { + batchedRequests.add(request) + + postBatchedRequests() + } + + private fun postBatchedRequests() { + val port = audioWorkletMessagePort + + if (port != null) { + for (request in batchedRequests) { + val message = when (serializer) { + is StringFormat -> { + serializer.encodeToString(WorkerRequest.serializer(), request) + } + + is BinaryFormat -> { + serializer.encodeToByteArray(WorkerRequest.serializer(), request) + } + + else -> { + error("Unknown serializer format ${serializer::class.simpleName}") + } + } + + port.postMessage(message) + } + + batchedRequests.clear() + } + }*/ +} + +object AudioWorkletHandler : AudioWorklet( + "static/worklet-processor.js", + "static/audio-worklet.js" +) { + + override fun createNode(audioContext: dynamic): dynamic = js( + // worklet-processor as defined in de javascript: + // registerProcessor('worklet-processor', WorkletProcessor); + "new AudioWorkletNode(audioContext, 'worklet-processor', { numberOfInputs: 0, outputChannelCount: [2] })" + ) + + override fun onAudioWorkletMessage(message: MessageEvent) { + console.log("Received message from audio worklet: ", message) + } + + override fun onCodeLoaded() { + println("Audio worklet code is loaded.") + } + + fun start() { + audioWorkletMessagePort?.postMessage("start") + } + +} diff --git a/src/jvmMain/kotlin/nl.astraeus.application/Server.kt b/src/jvmMain/kotlin/nl.astraeus.application/Server.kt new file mode 100644 index 0000000..af5d69d --- /dev/null +++ b/src/jvmMain/kotlin/nl.astraeus.application/Server.kt @@ -0,0 +1,44 @@ +package nl.astraeus.application + +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.* +import io.ktor.server.engine.embeddedServer +import io.ktor.server.html.* +import io.ktor.server.http.content.* +import io.ktor.server.netty.Netty +import io.ktor.server.routing.* +import kotlinx.html.* + +fun HTML.index() { + head { + title("Hello from Ktor!") + link("static/worklet.css", "stylesheet" ,"text/css") + } + body { + div { + +"Hello from Ktor" + } + div { + id = "root" + } + span("button") { + id = "clicker" + + + "Start" + } + script(src = "/static/kotlin-audioworklet.js") {} + } +} + +fun main() { + embeddedServer(Netty, port = 8080, host = "127.0.0.1") { + routing { + get("/") { + call.respondHtml(HttpStatusCode.OK, HTML::index) + } + static("/static") { + resources() + } + } + }.start(wait = true) +}