Initial commit

This commit is contained in:
2022-12-22 16:27:24 +01:00
commit a05807667f
14 changed files with 744 additions and 0 deletions

View File

@@ -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)
// }
}
}

View File

@@ -0,0 +1,54 @@
; // make sure kotlin js code last statement is ended
class WorkletProcessor extends AudioWorkletProcessor {
// Static getter to define AudioParam objects in this custom processor.
static get parameterDescriptors() {
return [{
name: 'volume',
defaultValue: 0.75
}];
}
constructor() {
super();
console.log("worklet-processor.constructor", this, audioWorklet);
audioWorklet.nl.astraeus.processor.WorkletProcessor.setPort(this.port);
console.log("STARTED worklet-processor.js");
}
process(inputs, outputs, parameters) {
let result = true;
let samplesToProcess = 0;
if (outputs.length !== 1) {
result = false;
console.log("Unexpected number of outputs!", outputs)
} else {
let channels = outputs[0].length;
if (channels !== 2) {
result = false;
console.log("Unexpected number of channels!", outputs[0]);
} else if (outputs[0][0].length !== outputs[0][1].length) {
result = false;
console.log("Channels have different lengths!!", outputs[0]);
} else {
samplesToProcess = outputs[0][0].length;
audioWorklet.nl.astraeus.processor.WorkletProcessor.process(
samplesToProcess,
outputs[0][0],
outputs[0][1]
);
}
}
return result;
}
}
registerProcessor('worklet-processor', WorkletProcessor);

View File

@@ -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()
}
}, "")
}
}

View File

@@ -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")
}
}

View File

@@ -0,0 +1,4 @@
.button {
padding: 3px 5px;
background-color: red;
}

View File

@@ -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)
}