Initial commit
This commit is contained in:
@@ -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)
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
54
src/jsAudioWorkletMain/resources/worklet-processor.js
Normal file
54
src/jsAudioWorkletMain/resources/worklet-processor.js
Normal 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);
|
||||
21
src/jsMain/kotlin/nl/astraeus/Main.kt
Normal file
21
src/jsMain/kotlin/nl/astraeus/Main.kt
Normal 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()
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
|
||||
}
|
||||
162
src/jsMain/kotlin/nl/astraeus/handler/AudioWorkletHandler.kt
Normal file
162
src/jsMain/kotlin/nl/astraeus/handler/AudioWorkletHandler.kt
Normal 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")
|
||||
}
|
||||
|
||||
}
|
||||
4
src/jsMain/resources/worklet.css
Normal file
4
src/jsMain/resources/worklet.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.button {
|
||||
padding: 3px 5px;
|
||||
background-color: red;
|
||||
}
|
||||
44
src/jvmMain/kotlin/nl.astraeus.application/Server.kt
Normal file
44
src/jvmMain/kotlin/nl.astraeus.application/Server.kt
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user