Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7449c86fd | |||
| 223ff91dde | |||
| 6f03f71b15 | |||
| a9e631055b |
@@ -1,10 +1,9 @@
|
||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target.VAR
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.LEGACY
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJsCompile
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.IR
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target.VAR
|
||||
|
||||
plugins {
|
||||
kotlin("multiplatform") version "1.8.10"
|
||||
kotlin("multiplatform") version "2.0.0"
|
||||
application
|
||||
}
|
||||
|
||||
@@ -18,6 +17,13 @@ repositories {
|
||||
|
||||
val jsMode = IR
|
||||
|
||||
tasks.withType<KotlinJsCompile>().configureEach {
|
||||
kotlinOptions {
|
||||
moduleKind = "es"
|
||||
useEsClasses = true
|
||||
}
|
||||
}
|
||||
|
||||
kotlin {
|
||||
jvm {
|
||||
compilations.all {
|
||||
@@ -32,6 +38,15 @@ kotlin {
|
||||
binaries.executable()
|
||||
browser()
|
||||
}
|
||||
js("jsWorklet", jsMode) {
|
||||
binaries.executable()
|
||||
|
||||
browser {
|
||||
commonWebpackConfig {
|
||||
outputFileName = "html-worklet.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
js("jsAudioWorklet", jsMode) {
|
||||
binaries.executable()
|
||||
|
||||
@@ -66,9 +81,10 @@ application {
|
||||
}
|
||||
|
||||
tasks.named<Copy>("jvmProcessResources") {
|
||||
val jsBrowserDistribution = tasks.named("jsBrowserDevelopmentWebpack")
|
||||
val jsBrowserDistribution = tasks.named("jsBrowserDistribution")
|
||||
from(jsBrowserDistribution)
|
||||
from(tasks.named("jsAudioWorkletBrowserDistribution"))
|
||||
from(tasks.named("jsWorkletBrowserDistribution"))
|
||||
}
|
||||
|
||||
tasks.named<JavaExec>("run") {
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* User: rnentjes
|
||||
* Date: 14-11-15
|
||||
* Time: 11:50
|
||||
*/
|
||||
|
||||
var sampleRate: Int = 44100
|
||||
|
||||
enum class Note(
|
||||
val description: String
|
||||
) {
|
||||
C0("C-0"),
|
||||
C0s("C#0"),
|
||||
D0("D-0"),
|
||||
D0s("D#0"),
|
||||
E0("E-0"),
|
||||
F0("F-0"),
|
||||
F0s("F#0"),
|
||||
G0("G-0"),
|
||||
G0s("G#0"),
|
||||
A0("A-0"),
|
||||
A0s("A#0"),
|
||||
B0("B-0"),
|
||||
C1("C-1"),
|
||||
C1s("C#1"),
|
||||
D1("D-1"),
|
||||
D1s("D#1"),
|
||||
E1("E-1"),
|
||||
F1("F-1"),
|
||||
F1s("F#1"),
|
||||
G1("G-1"),
|
||||
G1s("G#1"),
|
||||
A1("A-1"),
|
||||
A1s("A#1"),
|
||||
B1("B-1"),
|
||||
C2("C-2"),
|
||||
C2s("C#2"),
|
||||
D2("D-2"),
|
||||
D2s("D#2"),
|
||||
E2("E-2"),
|
||||
F2("F-2"),
|
||||
F2s("F#2"),
|
||||
G2("G-2"),
|
||||
G2s("G#2"),
|
||||
A2("A-2"),
|
||||
A2s("A#2"),
|
||||
B2("B-2"),
|
||||
C3("C-3"),
|
||||
C3s("C#3"),
|
||||
D3("D-3"),
|
||||
D3s("D#3"),
|
||||
E3("E-3"),
|
||||
F3("F-3"),
|
||||
F3s("F#3"),
|
||||
G3("G-3"),
|
||||
G3s("G#3"),
|
||||
A3("A-3"),
|
||||
A3s("A#3"),
|
||||
B3("B-3"),
|
||||
C4("C-4"),
|
||||
C4s("C#4"),
|
||||
D4("D-4"),
|
||||
D4s("D#4"),
|
||||
E4("E-4"),
|
||||
F4("F-4"),
|
||||
F4s("F#4"),
|
||||
G4("G-4"),
|
||||
G4s("G#4"),
|
||||
A4("A-4"),
|
||||
A4s("A#4"),
|
||||
B4("B-4"),
|
||||
C5("C-5"),
|
||||
C5s("C#5"),
|
||||
D5("D-5"),
|
||||
D5s("D#5"),
|
||||
E5("E-5"),
|
||||
F5("F-5"),
|
||||
F5s("F#5"),
|
||||
G5("G-5"),
|
||||
G5s("G#5"),
|
||||
A5("A-5"),
|
||||
A5s("A#5"),
|
||||
B5("B-5"),
|
||||
C6("C-6"),
|
||||
C6s("C#6"),
|
||||
D6("D-6"),
|
||||
D6s("D#6"),
|
||||
E6("E-6"),
|
||||
F6("F-6"),
|
||||
F6s("F#6"),
|
||||
G6("G-6"),
|
||||
G6s("G#6"),
|
||||
A6("A-6"),
|
||||
A6s("A#6"),
|
||||
B6("B-6"),
|
||||
C7("C-7"),
|
||||
C7s("C#7"),
|
||||
D7("D-7"),
|
||||
D7s("D#7"),
|
||||
E7("E-7"),
|
||||
F7("F-7"),
|
||||
F7s("F#7"),
|
||||
G7("G-7"),
|
||||
G7s("G#7"),
|
||||
A7("A-7"),
|
||||
A7s("A#7"),
|
||||
B7("B-7"),
|
||||
C8("C-8"),
|
||||
C8s("C#8"),
|
||||
D8("D-8"),
|
||||
D8s("D#8"),
|
||||
E8("E-8"),
|
||||
F8("F-8"),
|
||||
F8s("F#8"),
|
||||
G8("G-8"),
|
||||
G8s("G#8"),
|
||||
A8("A-8"),
|
||||
A8s("A#8"),
|
||||
B8("B-8"),
|
||||
NONE("---"),
|
||||
END("XXX"),
|
||||
UP("^^^"),
|
||||
;
|
||||
|
||||
val freq: Double by lazy {
|
||||
val ordinal = ordinal
|
||||
val relNote = ordinal - A4.ordinal
|
||||
|
||||
440.0 * 2.0.pow(relNote/12.0)
|
||||
}
|
||||
val cycleLength: Double by lazy { 1.0 / freq }
|
||||
val sampleDelta: Double by lazy { (1.0 / sampleRate.toDouble()) / cycleLength }
|
||||
|
||||
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= B8.ordinal) {
|
||||
var result = this.ordinal + semiNotes
|
||||
|
||||
result = min(result, B8.ordinal)
|
||||
result = max(result, C0.ordinal)
|
||||
|
||||
values().firstOrNull { it.ordinal == result } ?: this
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
/*
|
||||
* Amount of one cycle to advance per sample
|
||||
*/
|
||||
/*
|
||||
fun sampleDelta(): Double {
|
||||
// 44100
|
||||
val sampleRate = sampleRate
|
||||
val time = 1f / sampleRate.toDouble()
|
||||
|
||||
return time / cycleLength
|
||||
}
|
||||
*/
|
||||
|
||||
}
|
||||
179
src/commonMain/kotlin/common/Note.kt
Normal file
179
src/commonMain/kotlin/common/Note.kt
Normal file
@@ -0,0 +1,179 @@
|
||||
package common
|
||||
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/**
|
||||
* User: rnentjes
|
||||
* Date: 14-11-15
|
||||
* Time: 11:50
|
||||
*/
|
||||
|
||||
var sampleRate = 44100
|
||||
set(value) {
|
||||
field = value
|
||||
for (note in Note.values()) {
|
||||
note.sampleDelta = (1.0 / sampleRate.toDouble()) / note.cycleLength
|
||||
}
|
||||
}
|
||||
//var samplesPerEntry = 5512
|
||||
|
||||
enum class Note(
|
||||
val sharp: String,
|
||||
val flat: String
|
||||
) {
|
||||
NONE("---", "---"),
|
||||
NO02("",""),
|
||||
NO03("",""),
|
||||
NO04("",""),
|
||||
NO05("",""),
|
||||
NO06("",""),
|
||||
NO07("",""),
|
||||
NO08("",""),
|
||||
NO09("",""),
|
||||
NO10("",""),
|
||||
NO11("",""),
|
||||
NO12("",""),
|
||||
C0("C-0","C-0"),
|
||||
C0s("C#0","Db0"),
|
||||
D0("D-0","D-0"),
|
||||
D0s("D#0","Eb0"),
|
||||
E0("E-0","E-0"),
|
||||
F0("F-0","F-0"),
|
||||
F0s("F#0","Gb0"),
|
||||
G0("G-0","G-0"),
|
||||
G0s("G#0","Ab0"),
|
||||
A0("A-0","A-0"),
|
||||
A0s("A#0","Bb0"),
|
||||
B0("B-0","B-0"),
|
||||
C1("C-1","C-1"),
|
||||
C1s("C#1","Db1"),
|
||||
D1("D-1","D-1"),
|
||||
D1s("D#1","Eb1"),
|
||||
E1("E-1","E-1"),
|
||||
F1("F-1","F-1"),
|
||||
F1s("F#1","Gb1"),
|
||||
G1("G-1","G-1"),
|
||||
G1s("G#1","Ab1"),
|
||||
A1("A-1","A-1"),
|
||||
A1s("A#1","Bb1"),
|
||||
B1("B-1","B-1"),
|
||||
C2("C-2","C-2"),
|
||||
C2s("C#2","Db2"),
|
||||
D2("D-2","D-2"),
|
||||
D2s("D#2","Eb2"),
|
||||
E2("E-2","E-2"),
|
||||
F2("F-2","F-2"),
|
||||
F2s("F#2","Gb2"),
|
||||
G2("G-2","G-2"),
|
||||
G2s("G#2","Ab2"),
|
||||
A2("A-2","A-2"),
|
||||
A2s("A#2","Bb2"),
|
||||
B2("B-2","B-2"),
|
||||
C3("C-3","C-3"),
|
||||
C3s("C#3","Db3"),
|
||||
D3("D-3","D-3"),
|
||||
D3s("D#3","Eb3"),
|
||||
E3("E-3","E-3"),
|
||||
F3("F-3","F-3"),
|
||||
F3s("F#3","Gb3"),
|
||||
G3("G-3","G-3"),
|
||||
G3s("G#3","Ab3"),
|
||||
A3("A-3","A-3"),
|
||||
A3s("A#3","Bb3"),
|
||||
B3("B-3","B-3"),
|
||||
C4("C-4","C-4"),
|
||||
C4s("C#4","Db4"),
|
||||
D4("D-4","D-4"),
|
||||
D4s("D#4","Eb4"),
|
||||
E4("E-4","E-4"),
|
||||
F4("F-4","F-4"),
|
||||
F4s("F#4","Gb4"),
|
||||
G4("G-4","G-4"),
|
||||
G4s("G#4","Ab4"),
|
||||
A4("A-4","A-4"),
|
||||
A4s("A#4","Bb4"),
|
||||
B4("B-4","B-4"),
|
||||
C5("C-5","C-5"),
|
||||
C5s("C#5","Db5"),
|
||||
D5("D-5","D-5"),
|
||||
D5s("D#5","Eb5"),
|
||||
E5("E-5","E-5"),
|
||||
F5("F-5","F-5"),
|
||||
F5s("F#5","Gb5"),
|
||||
G5("G-5","G-5"),
|
||||
G5s("G#5","Ab5"),
|
||||
A5("A-5","A-5"),
|
||||
A5s("A#5","Bb5"),
|
||||
B5("B-5","B-5"),
|
||||
C6("C-6","C-6"),
|
||||
C6s("C#6","Db6"),
|
||||
D6("D-6","D-6"),
|
||||
D6s("D#6","Eb6"),
|
||||
E6("E-6","E-6"),
|
||||
F6("F-6","F-6"),
|
||||
F6s("F#6","Gb6"),
|
||||
G6("G-6","G-6"),
|
||||
G6s("G#6","Ab6"),
|
||||
A6("A-6","A-6"),
|
||||
A6s("A#6","Bb6"),
|
||||
B6("B-6","B-6"),
|
||||
C7("C-7","C-7"),
|
||||
C7s("C#7","Db7"),
|
||||
D7("D-7","D-7"),
|
||||
D7s("D#7","Eb7"),
|
||||
E7("E-7","E-7"),
|
||||
F7("F-7","F-7"),
|
||||
F7s("F#7","Gb7"),
|
||||
G7("G-7","G-7"),
|
||||
G7s("G#7","Ab7"),
|
||||
A7("A-7","A-7"),
|
||||
A7s("A#7","Bb7"),
|
||||
B7("B-7","B-7"),
|
||||
C8("C-8","C-8"),
|
||||
C8s("C#8","Db8"),
|
||||
D8("D-8","D-8"),
|
||||
D8s("D#8","Eb8"),
|
||||
E8("E-8","E-8"),
|
||||
F8("F-8","F-8"),
|
||||
F8s("F#8","Gb8"),
|
||||
G8("G-8","G-8"),
|
||||
G8s("G#8","Ab8"),
|
||||
A8("A-8","A-8"),
|
||||
A8s("A#8","Bb8"),
|
||||
B8("B-8","B-8"),
|
||||
C9("C-9","C-9"),
|
||||
C9s("C#9","Db9"),
|
||||
D9("D-9","D-9"),
|
||||
D9s("D#9","Eb9"),
|
||||
E9("E-9","E-9"),
|
||||
F9("F-9","F-9"),
|
||||
F9s("F#9","Gb9"),
|
||||
G9("G-9","G-9"),
|
||||
// out of midi range
|
||||
//G9s("G#9","Ab9"),
|
||||
//A9("A-9","A-9"),
|
||||
//A9s("A#9","Bb9"),
|
||||
//B9("B-9","B-9"),
|
||||
UP("^^^","^^^"),
|
||||
END("XXX","XXX"),
|
||||
;
|
||||
|
||||
// 69 = A4.ordinal
|
||||
val freq: Double = 440.0 * 2.0.pow((ordinal - 69)/12.0)
|
||||
val cycleLength: Double = 1.0 / freq
|
||||
var sampleDelta: Double = (1.0 / sampleRate.toDouble()) / cycleLength
|
||||
|
||||
fun transpose(semiNotes: Int): Note = if (ordinal >= C0.ordinal && ordinal <= G9.ordinal) {
|
||||
var result = this.ordinal + semiNotes
|
||||
|
||||
result = min(result, G9.ordinal)
|
||||
result = max(result, C0.ordinal)
|
||||
|
||||
values().firstOrNull { it.ordinal == result } ?: this
|
||||
} else {
|
||||
this
|
||||
}
|
||||
|
||||
}
|
||||
130
src/jsAudioWorkletMain/kotlin/AudioProcessor.kt
Normal file
130
src/jsAudioWorkletMain/kotlin/AudioProcessor.kt
Normal file
@@ -0,0 +1,130 @@
|
||||
import common.Note
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.MessagePort
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
const val PI2 = PI * 2
|
||||
|
||||
class NoteState(
|
||||
note: Note,
|
||||
counter: Int
|
||||
)
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
class AudioProcessor : AudioWorkletProcessor() {
|
||||
private var started = true
|
||||
private var counter: Int = 0
|
||||
private var note: Note? = null
|
||||
private var offset = 0.0
|
||||
|
||||
private var note_length = 2500
|
||||
private var harmonics = 3
|
||||
private var transpose = 0
|
||||
|
||||
private var workletPort: MessagePort? = null
|
||||
|
||||
init {
|
||||
this.port.onmessage = ::handleMessage
|
||||
|
||||
common.sampleRate = sampleRate
|
||||
}
|
||||
|
||||
private fun handleMessage(message: MessageEvent) {
|
||||
console.log("AudioProcessor: Received message", message)
|
||||
|
||||
val data: Any? = message.data
|
||||
if (data is String) {
|
||||
val parts = data.split("\n")
|
||||
when (parts[0]) {
|
||||
"start" -> {
|
||||
println("Start worklet!")
|
||||
|
||||
common.sampleRate = sampleRate
|
||||
started = true
|
||||
}
|
||||
"stop" -> {
|
||||
println("Stop worklet!")
|
||||
|
||||
started = false
|
||||
}
|
||||
"set_note_length" -> {
|
||||
note_length = parts[1].toInt()
|
||||
}
|
||||
"harmonics" -> {
|
||||
harmonics = parts[1].toInt()
|
||||
}
|
||||
"transpose" -> {
|
||||
transpose = parts[1].toInt()
|
||||
}
|
||||
"play" -> {
|
||||
note = Note.valueOf(parts[1])
|
||||
counter=1
|
||||
offset = 0.0
|
||||
}
|
||||
else ->
|
||||
console.error("Don't kow how to handle message", message)
|
||||
}
|
||||
} else {
|
||||
val dynData: dynamic = message.data
|
||||
if (dynData.command == "audio-iframe-message-port") {
|
||||
workletPort = dynData.port
|
||||
workletPort?.onmessage = {
|
||||
console.log("AudioProcessor: Received message from iframe", it)
|
||||
|
||||
handleMessage(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun process(
|
||||
inputs: Array<Array<Float32Array>>,
|
||||
outputs: Array<Array<Float32Array>>,
|
||||
parameters: dynamic
|
||||
) : Boolean {
|
||||
if (started) {
|
||||
check(outputs.size == 1) {
|
||||
"Expected 1 output got ${outputs.size}"
|
||||
}
|
||||
check(outputs[0].size == 2) {
|
||||
"Expected 2 output channels, got ${outputs.size}"
|
||||
}
|
||||
|
||||
note?.also { activeNote ->
|
||||
val delta = activeNote.sampleDelta
|
||||
val samples = outputs[0][0].length
|
||||
val left = outputs[0][0]
|
||||
val right = outputs[0][1]
|
||||
|
||||
for (sample in 0..<samples) {
|
||||
var value = sin(offset * PI2)
|
||||
|
||||
for (index in 0..<harmonics) {
|
||||
value += sin(offset * (index + 2) * PI2) * (3.0 / (index + 2))
|
||||
}
|
||||
offset += delta
|
||||
|
||||
// new note every NOTE_LENGTH samples
|
||||
val noteProgress = counter % note_length
|
||||
// simple envelop from max to 0 every note
|
||||
value *= (1.0 - noteProgress / note_length.toDouble())
|
||||
|
||||
left[sample] = value.toFloat()
|
||||
right[sample] = value.toFloat()
|
||||
|
||||
if (counter >= note_length) {
|
||||
note = null
|
||||
}
|
||||
counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
42
src/jsAudioWorkletMain/kotlin/Externals.kt
Normal file
42
src/jsAudioWorkletMain/kotlin/Externals.kt
Normal file
@@ -0,0 +1,42 @@
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.w3c.dom.MessagePort
|
||||
|
||||
|
||||
enum class AutomationRate(
|
||||
val rate: String
|
||||
) {
|
||||
A_RATE("a-rate"),
|
||||
K_RATE("k-rate")
|
||||
}
|
||||
|
||||
interface AudioParam {
|
||||
var value: Double
|
||||
var automationRate: AutomationRate
|
||||
val defaultValue: Double
|
||||
val minValue: Double
|
||||
val maxValue: Double
|
||||
}
|
||||
|
||||
interface AudioParamMap {
|
||||
operator fun get(name: String): AudioParam
|
||||
}
|
||||
|
||||
abstract external class AudioWorkletProcessor {
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/parameters) */
|
||||
//val parameters: AudioParamMap;
|
||||
/** [MDN Reference](https://developer.mozilla.org/docs/Web/API/AudioWorkletNode/port) */
|
||||
@JsName("port")
|
||||
val port: MessagePort
|
||||
|
||||
@JsName("process")
|
||||
open fun process (
|
||||
inputs: Array<Array<Float32Array>>,
|
||||
outputs: Array<Array<Float32Array>>,
|
||||
parameters: dynamic
|
||||
) : Boolean
|
||||
|
||||
}
|
||||
|
||||
external fun registerProcessor(name: String, processorCtor: JsClass<*>)
|
||||
external val sampleRate: Int
|
||||
external val currentTime: Double
|
||||
7
src/jsAudioWorkletMain/kotlin/Main.kt
Normal file
7
src/jsAudioWorkletMain/kotlin/Main.kt
Normal file
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
@OptIn(ExperimentalJsExport::class)
|
||||
fun main() {
|
||||
registerProcessor("audio-processor", AudioProcessor::class.js)
|
||||
registerProcessor("mixer-processor", MixerProcessor::class.js)
|
||||
}
|
||||
66
src/jsAudioWorkletMain/kotlin/MixerProcessor.kt
Normal file
66
src/jsAudioWorkletMain/kotlin/MixerProcessor.kt
Normal file
@@ -0,0 +1,66 @@
|
||||
import org.khronos.webgl.Float32Array
|
||||
import org.khronos.webgl.get
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.MessageEvent
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
class MixerProcessor : AudioWorkletProcessor() {
|
||||
var started = false
|
||||
val leftBuffer = Float32Array(sampleRate * 2)
|
||||
val rightBuffer = Float32Array(sampleRate * 2)
|
||||
|
||||
init {
|
||||
this.port.onmessage = ::handleMessage
|
||||
}
|
||||
|
||||
private fun handleMessage(message: MessageEvent) {
|
||||
console.log("MixerProcessor: Received message", message)
|
||||
|
||||
val data = message.data
|
||||
if (data is String) {
|
||||
val parts = data.split("\n")
|
||||
when (parts[0]) {
|
||||
"start" -> {
|
||||
println("Start worklet!")
|
||||
|
||||
started = true
|
||||
}
|
||||
"stop" -> {
|
||||
println("Stop worklet!")
|
||||
|
||||
started = false
|
||||
}
|
||||
else ->
|
||||
console.error("Don't kow how to handle message", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun process(
|
||||
inputs: Array<Array<Float32Array>>,
|
||||
outputs: Array<Array<Float32Array>>,
|
||||
parameters: dynamic
|
||||
) : Boolean {
|
||||
if (started) {
|
||||
check(inputs.size == 2) {
|
||||
"Expected 2 inputs, got ${inputs.size}"
|
||||
}
|
||||
check(outputs.size == 1) {
|
||||
"Expected 1 output, got ${outputs.size}"
|
||||
}
|
||||
val left = outputs[0][0]
|
||||
val right = outputs[0][1]
|
||||
|
||||
for (input in inputs.indices) {
|
||||
for (index in 0 ..< left.length) {
|
||||
left[index] = left[index] + inputs[input][0][index]
|
||||
right[index] = right[index] + inputs[input][1][index]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
@file:OptIn(ExperimentalJsExport::class)
|
||||
|
||||
import org.khronos.webgl.Float64Array
|
||||
import org.khronos.webgl.set
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.MessagePort
|
||||
import kotlin.math.PI
|
||||
import kotlin.math.sin
|
||||
|
||||
const val PI2 = PI * 2
|
||||
|
||||
@ExperimentalJsExport
|
||||
@JsExport
|
||||
object WorkletProcessor {
|
||||
private var port: MessagePort? = null
|
||||
|
||||
private var counter: Int = 0
|
||||
private var note = Note.C2
|
||||
private var offset = 0.0
|
||||
|
||||
private var started = false
|
||||
private var note_length = 2500
|
||||
private var harmonics = 3
|
||||
|
||||
@JsName("setPort")
|
||||
fun setPort(port: MessagePort) {
|
||||
WorkletProcessor.port = port
|
||||
WorkletProcessor.port?.onmessage = WorkletProcessor::onMessage
|
||||
}
|
||||
|
||||
@JsName("onMessage")
|
||||
fun onMessage(message: MessageEvent) {
|
||||
console.log("WorkletProcessor: Received message", message)
|
||||
|
||||
val data = message.data
|
||||
if (data is String) {
|
||||
val parts = data.split("\n")
|
||||
when (parts[0]) {
|
||||
"start" -> {
|
||||
println("Start worklet!")
|
||||
|
||||
started = true
|
||||
}
|
||||
"stop" -> {
|
||||
println("Stop worklet!")
|
||||
|
||||
started = false
|
||||
}
|
||||
"set_note_length" -> {
|
||||
note_length = parts[1].toInt()
|
||||
}
|
||||
"harmonics" -> {
|
||||
harmonics = parts[1].toInt()
|
||||
}
|
||||
else ->
|
||||
console.error("Don't kow how to handle message", message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JsName("process")
|
||||
fun process(samples: Int, left: Float64Array, right: Float64Array) {
|
||||
if (started) {
|
||||
var tmpCounter = counter
|
||||
var delta = note.sampleDelta
|
||||
|
||||
for (sample in 0 until samples) {
|
||||
var value = sin(offset * PI2);
|
||||
|
||||
for(index in 0 until harmonics) {
|
||||
value += sin(offset * (index + 2) * PI2) * (1.0 / (index+2))
|
||||
}
|
||||
offset += delta
|
||||
|
||||
// new note every NOTE_LENGTH samples
|
||||
val noteProgress = tmpCounter % note_length
|
||||
if (noteProgress == 0) {
|
||||
note = note.transpose(1)
|
||||
if (note == Note.C7) {
|
||||
note = Note.C2
|
||||
}
|
||||
delta = note.sampleDelta
|
||||
}
|
||||
// simple envelop from max to 0 every note
|
||||
value *= (1.0 - noteProgress / note_length.toDouble())
|
||||
|
||||
left[sample] = value
|
||||
right[sample] = value
|
||||
tmpCounter++
|
||||
}
|
||||
|
||||
counter += samples
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
; // 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.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.WorkletProcessor.process(
|
||||
samplesToProcess,
|
||||
outputs[0][0],
|
||||
outputs[0][1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('worklet-processor', WorkletProcessor);
|
||||
71
src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt
Normal file
71
src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt
Normal file
@@ -0,0 +1,71 @@
|
||||
package nl.astraeus
|
||||
|
||||
import common.Note
|
||||
import nl.astraeus.handler.AudioNode
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.MessagePort
|
||||
|
||||
class AudioProcessorNode(
|
||||
audioModule: dynamic,
|
||||
destination: dynamic,
|
||||
outputIndex: Int = 0,
|
||||
inputIndex: Int = 0
|
||||
) : AudioNode(
|
||||
audioModule,
|
||||
"audio-processor",
|
||||
0,
|
||||
arrayOf(2),
|
||||
destination,
|
||||
outputIndex,
|
||||
inputIndex
|
||||
) {
|
||||
override fun onMessage(message: MessageEvent) {
|
||||
console.log("Got message from audio worklet", message)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (!created) {
|
||||
create {
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
node?.port.postMessage("start")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!created) {
|
||||
create {
|
||||
stop()
|
||||
}
|
||||
} else {
|
||||
node?.port.postMessage("stop")
|
||||
}
|
||||
}
|
||||
|
||||
fun harmonic(i: Int) {
|
||||
node?.port.postMessage("harmonics\n$i")
|
||||
}
|
||||
|
||||
fun transpose(i: Int) {
|
||||
node?.port.postMessage("transpose\n$i")
|
||||
}
|
||||
|
||||
fun length(i: Int) {
|
||||
node?.port.postMessage("set_note_length\n$i")
|
||||
}
|
||||
|
||||
fun play(note: Note) {
|
||||
node?.port.postMessage("play\n$note")
|
||||
}
|
||||
|
||||
fun setWorkletPort(port2: MessagePort) {
|
||||
node?.port.postMessage(
|
||||
createWorkletSetPortMessage(
|
||||
"audio-iframe-message-port",
|
||||
port2
|
||||
),
|
||||
arrayOf(port2)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,81 @@
|
||||
package nl.astraeus
|
||||
|
||||
import common.Note
|
||||
import kotlinx.browser.document
|
||||
import nl.astraeus.handler.AudioWorkletHandler
|
||||
import kotlinx.browser.window
|
||||
import nl.astraeus.handler.AudioModule
|
||||
import org.w3c.dom.HTMLIFrameElement
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.MessageChannel
|
||||
import org.w3c.dom.MessagePort
|
||||
|
||||
fun createWorkletSetPortMessage(
|
||||
command: String,
|
||||
port: MessagePort
|
||||
): dynamic {
|
||||
val result = js("{}")
|
||||
|
||||
result.command = command
|
||||
result.port = port
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
fun main() {
|
||||
AudioWorkletHandler.loadCode()
|
||||
val audioModule = AudioModule("static/audio-worklet.js")
|
||||
val mixer = MixerProcessorNode(audioModule)
|
||||
var node1: AudioProcessorNode? = null
|
||||
var node2: AudioProcessorNode? = null
|
||||
val iframeWorkletChannel = MessageChannel()
|
||||
|
||||
println("Ok")
|
||||
window.addEventListener("message", { event ->
|
||||
console.log("Main Received message: ", event)
|
||||
}, "")
|
||||
//window.postMessage("Hello from Main 1", window.location.origin + "/worklet.html")
|
||||
|
||||
document.getElementById("iframe")?.also {
|
||||
val iframe = it as? HTMLIFrameElement
|
||||
iframe?.also {
|
||||
it.contentWindow?.postMessage("Hello from Main 2", window.location.origin + "/worklet.html")
|
||||
it.contentWindow?.postMessage(
|
||||
createWorkletSetPortMessage(
|
||||
"audio-processor-message-port",
|
||||
iframeWorkletChannel.port1
|
||||
),
|
||||
window.location.origin + "/worklet.html",
|
||||
arrayOf(iframeWorkletChannel.port1)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("createButton")?.also {
|
||||
it.addEventListener("click", {
|
||||
mixer.create {
|
||||
node1 = AudioProcessorNode(audioModule, mixer.node)
|
||||
node2 = AudioProcessorNode(audioModule, mixer.node, 0,1)
|
||||
|
||||
node1?.create {
|
||||
println("node 1 created")
|
||||
node1?.setWorkletPort(iframeWorkletChannel.port2)
|
||||
}
|
||||
node2?.create {
|
||||
println("node 2 created")
|
||||
|
||||
node2?.transpose(7)
|
||||
}
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
|
||||
document.getElementById("startButton")?.also {
|
||||
it.addEventListener("click", {
|
||||
if (AudioWorkletHandler.audioContext == null) {
|
||||
AudioWorkletHandler.createContext {
|
||||
println("Created context")
|
||||
|
||||
AudioWorkletHandler.start()
|
||||
}
|
||||
} else {
|
||||
AudioWorkletHandler.start()
|
||||
}
|
||||
mixer.start()
|
||||
}, "")
|
||||
}
|
||||
|
||||
document.getElementById("stopButton")?.also {
|
||||
it.addEventListener("click", {
|
||||
AudioWorkletHandler.stop()
|
||||
mixer.stop()
|
||||
}, "")
|
||||
}
|
||||
|
||||
@@ -33,17 +83,49 @@ fun main() {
|
||||
it.addEventListener("change", {
|
||||
val target = it.target
|
||||
if (target is HTMLInputElement) {
|
||||
AudioWorkletHandler.setNoteLength(target.value.toInt())
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("harmonics")?.also {
|
||||
it.addEventListener("change", {
|
||||
val target = it.target
|
||||
if (target is HTMLInputElement) {
|
||||
AudioWorkletHandler.setHarmonics(target.value.toInt())
|
||||
node1?.length(target.value.toInt())
|
||||
node2?.length(target.value.toInt())
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
|
||||
document.getElementById("harmonics")?.also {
|
||||
it.addEventListener("change", {
|
||||
val target = it.target
|
||||
if (target is HTMLInputElement) {
|
||||
node1?.harmonic(target.value.toInt())
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_c3_1")?.also {
|
||||
it.addEventListener("click", {
|
||||
node1?.play(Note.C3)
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_e3_1")?.also {
|
||||
it.addEventListener("click", {
|
||||
node1?.play(Note.E3)
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_g3_1")?.also {
|
||||
it.addEventListener("click", {
|
||||
node1?.play(Note.G3)
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_c3_2")?.also {
|
||||
it.addEventListener("click", {
|
||||
node2?.play(Note.C3)
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_e3_2")?.also {
|
||||
it.addEventListener("click", {
|
||||
node2?.play(Note.E3)
|
||||
}, "")
|
||||
}
|
||||
document.getElementById("note_g3_2")?.also {
|
||||
it.addEventListener("click", {
|
||||
node2?.play(Note.G3)
|
||||
}, "")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
37
src/jsMain/kotlin/nl/astraeus/MixerProcessorNode.kt
Normal file
37
src/jsMain/kotlin/nl/astraeus/MixerProcessorNode.kt
Normal file
@@ -0,0 +1,37 @@
|
||||
package nl.astraeus
|
||||
|
||||
import nl.astraeus.handler.AudioNode
|
||||
import org.w3c.dom.MessageEvent
|
||||
|
||||
class MixerProcessorNode(
|
||||
audioModule: dynamic
|
||||
) : AudioNode(
|
||||
audioModule,
|
||||
"mixer-processor",
|
||||
2,
|
||||
arrayOf(2)
|
||||
) {
|
||||
override fun onMessage(message: MessageEvent) {
|
||||
console.log("Got message from audio worklet", message)
|
||||
}
|
||||
|
||||
fun start() {
|
||||
if (!created) {
|
||||
create {
|
||||
start()
|
||||
}
|
||||
} else {
|
||||
node?.port.postMessage("start")
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (!created) {
|
||||
create {
|
||||
stop()
|
||||
}
|
||||
} else {
|
||||
node?.port.postMessage("stop")
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt
Normal file
83
src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt
Normal file
@@ -0,0 +1,83 @@
|
||||
package nl.astraeus.handler
|
||||
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.MessagePort
|
||||
|
||||
enum class ModuleStatus {
|
||||
INIT,
|
||||
LOADING,
|
||||
READY
|
||||
}
|
||||
|
||||
class AudioModule(
|
||||
val jsFile: String
|
||||
) {
|
||||
var status = ModuleStatus.INIT
|
||||
var audioContext: dynamic = null
|
||||
var module: dynamic = null
|
||||
|
||||
// call from user gesture
|
||||
fun doAction(action: () -> Unit) {
|
||||
if (module == null && status == ModuleStatus.INIT) {
|
||||
status = ModuleStatus.LOADING
|
||||
if (audioContext == null) {
|
||||
audioContext = AudioContext()
|
||||
}
|
||||
module = audioContext.audioWorklet.addModule(
|
||||
jsFile
|
||||
)
|
||||
module.then {
|
||||
status = ModuleStatus.READY
|
||||
action()
|
||||
}
|
||||
} else if (status == ModuleStatus.READY) {
|
||||
action()
|
||||
} else {
|
||||
console.log("Module not yet loaded")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class AudioNode(
|
||||
val module: AudioModule,
|
||||
val processorName: String,
|
||||
val numberOfInputs: Int = 0,
|
||||
val outputChannelCount: Array<Int> = arrayOf(2),
|
||||
val destination: dynamic = null,
|
||||
val outputIndex: Int = 0,
|
||||
val inputIndex: Int = 0
|
||||
) {
|
||||
var created = false
|
||||
var node: dynamic = null
|
||||
var port: MessagePort? = null
|
||||
|
||||
abstract fun onMessage(message: MessageEvent)
|
||||
|
||||
// call from user gesture
|
||||
fun create(done: (node: dynamic) -> Unit) {
|
||||
module.doAction {
|
||||
node = AudioWorkletNode(
|
||||
module.audioContext,
|
||||
processorName,
|
||||
AudioWorkletNodeParameters(
|
||||
numberOfInputs,
|
||||
outputChannelCount
|
||||
)
|
||||
)
|
||||
|
||||
if (destination == null) {
|
||||
node.connect(module.audioContext.destination)
|
||||
} else {
|
||||
node.connect(destination, outputIndex, inputIndex)
|
||||
}
|
||||
|
||||
node.port.onmessage = ::onMessage
|
||||
|
||||
port = node.port as? MessagePort
|
||||
|
||||
created = true
|
||||
|
||||
done(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
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")
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
audioWorkletMessagePort?.postMessage("stop")
|
||||
}
|
||||
|
||||
fun setNoteLength(length: Int) {
|
||||
audioWorkletMessagePort?.postMessage("set_note_length\n$length")
|
||||
}
|
||||
|
||||
fun setHarmonics(length: Int) {
|
||||
audioWorkletMessagePort?.postMessage("harmonics\n$length")
|
||||
}
|
||||
}
|
||||
18
src/jsMain/kotlin/nl/astraeus/handler/Externals.kt
Normal file
18
src/jsMain/kotlin/nl/astraeus/handler/Externals.kt
Normal file
@@ -0,0 +1,18 @@
|
||||
package nl.astraeus.handler
|
||||
|
||||
external class AudioContext {
|
||||
var sampleRate: Int
|
||||
}
|
||||
|
||||
external class AudioWorkletNode(
|
||||
audioContext: dynamic,
|
||||
name: String,
|
||||
options: dynamic
|
||||
)
|
||||
|
||||
class AudioWorkletNodeParameters(
|
||||
@JsName("numberOfInputs")
|
||||
val numberOfInputs: Int,
|
||||
@JsName("outputChannelCount")
|
||||
val outputChannelCount: Array<Int>
|
||||
)
|
||||
43
src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt
Normal file
43
src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package nl.astraeus.worklet
|
||||
|
||||
import kotlinx.browser.document
|
||||
import kotlinx.browser.window
|
||||
import org.w3c.dom.HTMLInputElement
|
||||
import org.w3c.dom.MessageEvent
|
||||
import org.w3c.dom.MessagePort
|
||||
|
||||
fun main() {
|
||||
var port: MessagePort? = null
|
||||
console.log("Worklet", document.location)
|
||||
|
||||
window.addEventListener("message", { event ->
|
||||
console.log("Worklet xxx Received message: ", event)
|
||||
|
||||
if (event is MessageEvent) {
|
||||
val data: dynamic = event.data
|
||||
|
||||
console.log("WL DATA: ", data, data?.command == "audio-processor-message-port")
|
||||
if (data?.command == "audio-processor-message-port") {
|
||||
port = data.port
|
||||
port?.onmessage = {
|
||||
console.log("Worklet Received message from audio worklet: ", it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, "")
|
||||
|
||||
window.parent.postMessage(
|
||||
"Hello from Worklet",
|
||||
window.location.origin + "/index.html"
|
||||
)
|
||||
|
||||
document.getElementById("harmonics")?.also {
|
||||
it.addEventListener("change", {
|
||||
val target = it.target
|
||||
if (target is HTMLInputElement) {
|
||||
console.log("Sending: ", target.value, port)
|
||||
port?.postMessage("harmonics\n${target.value}")
|
||||
}
|
||||
}, "")
|
||||
}
|
||||
}
|
||||
125
src/jvmMain/kotlin/IndexHtml.kt
Normal file
125
src/jvmMain/kotlin/IndexHtml.kt
Normal file
@@ -0,0 +1,125 @@
|
||||
import kotlinx.html.HTML
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.body
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.h3
|
||||
import kotlinx.html.head
|
||||
import kotlinx.html.id
|
||||
import kotlinx.html.iframe
|
||||
import kotlinx.html.input
|
||||
import kotlinx.html.label
|
||||
import kotlinx.html.link
|
||||
import kotlinx.html.script
|
||||
import kotlinx.html.span
|
||||
import kotlinx.html.style
|
||||
import kotlinx.html.title
|
||||
|
||||
fun HTML.index() {
|
||||
head {
|
||||
title("Hello from Ktor!")
|
||||
link("static/worklet.css", "stylesheet" ,"text/css")
|
||||
}
|
||||
body {
|
||||
div {
|
||||
+"We need a button to start because we can only start audio from a user event:"
|
||||
}
|
||||
div("button_div") {
|
||||
span("button") {
|
||||
id = "createButton"
|
||||
|
||||
+"Create"
|
||||
}
|
||||
|
||||
span("button") {
|
||||
id = "startButton"
|
||||
|
||||
+"Start"
|
||||
}
|
||||
|
||||
span("button") {
|
||||
id = "stopButton"
|
||||
|
||||
+"Stop"
|
||||
}
|
||||
}
|
||||
div {
|
||||
+ "An example of how to interact with the audioworklet:"
|
||||
}
|
||||
div {
|
||||
label {
|
||||
htmlFor = "noteLength"
|
||||
+"Note length (in samples):"
|
||||
}
|
||||
input {
|
||||
id = "noteLength"
|
||||
type = InputType.number
|
||||
value = "2500"
|
||||
min = "1"
|
||||
max = "100000"
|
||||
step = "100"
|
||||
}
|
||||
}
|
||||
div {
|
||||
label {
|
||||
htmlFor = "harmonics"
|
||||
+"Number of harmonics:"
|
||||
}
|
||||
input {
|
||||
id = "harmonics"
|
||||
type = InputType.number
|
||||
value = "3"
|
||||
min = "0"
|
||||
max = "10"
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
+ "Node 1"
|
||||
}
|
||||
div {
|
||||
input {
|
||||
id = "note_c3_1"
|
||||
type = InputType.button
|
||||
value = "C3"
|
||||
}
|
||||
input {
|
||||
id = "note_e3_1"
|
||||
type = InputType.button
|
||||
value = "E3"
|
||||
}
|
||||
input {
|
||||
id = "note_g3_1"
|
||||
type = InputType.button
|
||||
value = "G3"
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
+ "Node 2"
|
||||
}
|
||||
div {
|
||||
input {
|
||||
id = "note_c3_2"
|
||||
type = InputType.button
|
||||
value = "C3"
|
||||
}
|
||||
input {
|
||||
id = "note_e3_2"
|
||||
type = InputType.button
|
||||
value = "E3"
|
||||
}
|
||||
input {
|
||||
id = "note_g3_2"
|
||||
type = InputType.button
|
||||
value = "G3"
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
id = "iframe"
|
||||
src = "/worklet.html"
|
||||
style = "width: 100%; height: 500px; background-color: #ddf;"
|
||||
}
|
||||
|
||||
script(src = "/static/kotlin-audioworklet.js") {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,71 +4,22 @@ 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.response.*
|
||||
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 {
|
||||
+"We need a button to start because we can only start audio from a user event:"
|
||||
}
|
||||
div("button_div") {
|
||||
span("button") {
|
||||
id = "startButton"
|
||||
|
||||
+"Start"
|
||||
}
|
||||
|
||||
span("button") {
|
||||
id = "stopButton"
|
||||
|
||||
+"Stop"
|
||||
}
|
||||
}
|
||||
div {
|
||||
+ "An example of how to interact with the audioworklet:"
|
||||
}
|
||||
div {
|
||||
label {
|
||||
htmlFor = "noteLength"
|
||||
+"Note length (in samples):"
|
||||
}
|
||||
input {
|
||||
id = "noteLength"
|
||||
type = InputType.number
|
||||
value = "2500"
|
||||
min = "1"
|
||||
max = "100000"
|
||||
step = "100"
|
||||
}
|
||||
}
|
||||
div {
|
||||
label {
|
||||
htmlFor = "harmonics"
|
||||
+"Number of harmonics:"
|
||||
}
|
||||
input {
|
||||
id = "harmonics"
|
||||
type = InputType.number
|
||||
value = "3"
|
||||
min = "1"
|
||||
max = "10"
|
||||
}
|
||||
}
|
||||
script(src = "/static/kotlin-audioworklet.js") {}
|
||||
}
|
||||
}
|
||||
|
||||
fun main() {
|
||||
embeddedServer(Netty, port = 8080, host = "127.0.0.1") {
|
||||
routing {
|
||||
get("/") {
|
||||
call.respondRedirect("/index.html")
|
||||
}
|
||||
get("/index.html") {
|
||||
call.respondHtml(HttpStatusCode.OK, HTML::index)
|
||||
}
|
||||
get("/worklet.html") {
|
||||
call.respondHtml(HttpStatusCode.OK, HTML::worklet)
|
||||
}
|
||||
static("/static") {
|
||||
resources()
|
||||
}
|
||||
|
||||
40
src/jvmMain/kotlin/WorkletHtml.kt
Normal file
40
src/jvmMain/kotlin/WorkletHtml.kt
Normal file
@@ -0,0 +1,40 @@
|
||||
import kotlinx.html.HTML
|
||||
import kotlinx.html.InputType
|
||||
import kotlinx.html.body
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.head
|
||||
import kotlinx.html.id
|
||||
import kotlinx.html.iframe
|
||||
import kotlinx.html.input
|
||||
import kotlinx.html.label
|
||||
import kotlinx.html.link
|
||||
import kotlinx.html.script
|
||||
import kotlinx.html.span
|
||||
import kotlinx.html.title
|
||||
|
||||
fun HTML.worklet() {
|
||||
head {
|
||||
title("Hello from Ktor!")
|
||||
link("static/worklet.css", "stylesheet" ,"text/css")
|
||||
}
|
||||
body {
|
||||
div {
|
||||
+"IFrame js set worklet parameters:"
|
||||
}
|
||||
div {
|
||||
label {
|
||||
htmlFor = "harmonics"
|
||||
+"Number of harmonics:"
|
||||
}
|
||||
input {
|
||||
id = "harmonics"
|
||||
type = InputType.number
|
||||
value = "3"
|
||||
min = "0"
|
||||
max = "10"
|
||||
}
|
||||
}
|
||||
|
||||
script(src = "/static/html-worklet.js") {}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user