diff --git a/build.gradle.kts b/build.gradle.kts index 49fda3d..67c026c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinJsCompilerType.IR import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackOutput.Target.VAR plugins { - kotlin("multiplatform") version "1.9.10" + kotlin("multiplatform") version "1.9.23" application } @@ -81,7 +81,7 @@ application { } tasks.named("jvmProcessResources") { - val jsBrowserDistribution = tasks.named("jsBrowserDevelopmentWebpack") + val jsBrowserDistribution = tasks.named("jsBrowserDistribution") from(jsBrowserDistribution) from(tasks.named("jsAudioWorkletBrowserDistribution")) from(tasks.named("jsWorkletBrowserDistribution")) diff --git a/src/commonMain/kotlin/Note.kt b/src/commonMain/kotlin/Note.kt deleted file mode 100644 index 803129b..0000000 --- a/src/commonMain/kotlin/Note.kt +++ /dev/null @@ -1,165 +0,0 @@ -import kotlin.math.max -import kotlin.math.min -import kotlin.math.pow - -/** - * User: rnentjes - * Date: 14-11-15 - * Time: 11:50 - */ - - -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 - } - - companion object { - var sampleRate: Int = 44100 - } - - /* - * Amount of one cycle to advance per sample - */ -/* - fun sampleDelta(): Double { - // 44100 - val sampleRate = sampleRate - val time = 1f / sampleRate.toDouble() - - return time / cycleLength - } -*/ - -} diff --git a/src/commonMain/kotlin/common/Note.kt b/src/commonMain/kotlin/common/Note.kt new file mode 100644 index 0000000..172c19b --- /dev/null +++ b/src/commonMain/kotlin/common/Note.kt @@ -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 + } + +} diff --git a/src/jsAudioWorkletMain/kotlin/AudioProcessor.kt b/src/jsAudioWorkletMain/kotlin/AudioProcessor.kt index e603b09..f88ba3a 100644 --- a/src/jsAudioWorkletMain/kotlin/AudioProcessor.kt +++ b/src/jsAudioWorkletMain/kotlin/AudioProcessor.kt @@ -1,3 +1,4 @@ +import common.Note import org.khronos.webgl.Float32Array import org.khronos.webgl.set import org.w3c.dom.MessageEvent @@ -28,6 +29,8 @@ class AudioProcessor : AudioWorkletProcessor() { init { this.port.onmessage = ::handleMessage + + common.sampleRate = sampleRate } private fun handleMessage(message: MessageEvent) { @@ -40,7 +43,7 @@ class AudioProcessor : AudioWorkletProcessor() { "start" -> { println("Start worklet!") - Note.sampleRate = sampleRate + common.sampleRate = sampleRate started = true } "stop" -> { @@ -92,7 +95,7 @@ class AudioProcessor : AudioWorkletProcessor() { } note?.also { activeNote -> - var delta = activeNote.sampleDelta + val delta = activeNote.sampleDelta val samples = outputs[0][0].length val left = outputs[0][0] val right = outputs[0][1] diff --git a/src/jsAudioWorkletMain/kotlin/Externals.kt b/src/jsAudioWorkletMain/kotlin/Externals.kt index 24b18c4..7087052 100644 --- a/src/jsAudioWorkletMain/kotlin/Externals.kt +++ b/src/jsAudioWorkletMain/kotlin/Externals.kt @@ -1,6 +1,26 @@ 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; @@ -19,3 +39,4 @@ abstract external class AudioWorkletProcessor { external fun registerProcessor(name: String, processorCtor: JsClass<*>) external val sampleRate: Int +external val currentTime: Double diff --git a/src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt b/src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt index df0a8e7..18800fe 100644 --- a/src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt +++ b/src/jsMain/kotlin/nl/astraeus/AudioProcessorNode.kt @@ -1,6 +1,6 @@ package nl.astraeus -import Note +import common.Note import nl.astraeus.handler.AudioNode import org.w3c.dom.MessageEvent import org.w3c.dom.MessagePort @@ -68,4 +68,4 @@ class AudioProcessorNode( arrayOf(port2) ) } -} \ No newline at end of file +} diff --git a/src/jsMain/kotlin/nl/astraeus/Main.kt b/src/jsMain/kotlin/nl/astraeus/Main.kt index 066b9bc..29377b6 100644 --- a/src/jsMain/kotlin/nl/astraeus/Main.kt +++ b/src/jsMain/kotlin/nl/astraeus/Main.kt @@ -1,5 +1,6 @@ package nl.astraeus +import common.Note import kotlinx.browser.document import kotlinx.browser.window import nl.astraeus.handler.AudioModule @@ -8,9 +9,6 @@ import org.w3c.dom.HTMLInputElement import org.w3c.dom.MessageChannel import org.w3c.dom.MessagePort - - - fun createWorkletSetPortMessage( command: String, port: MessagePort @@ -90,21 +88,42 @@ fun main() { } }, "") } - document.getElementById("note_c3")?.also { - it.addEventListener("click", { - node1?.play(Note.C3) - node2?.play(Note.C3) - }, "") - } - document.getElementById("note_e3")?.also { - it.addEventListener("click", { - node1?.play(Note.C3) - node2?.play(Note.G3) + + document.getElementById("harmonics")?.also { + it.addEventListener("change", { + val target = it.target + if (target is HTMLInputElement) { + node1?.harmonic(target.value.toInt()) + } }, "") } - document.getElementById("note_g3")?.also { + 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) }, "") } diff --git a/src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt b/src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt index a52585b..e9c8841 100644 --- a/src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt +++ b/src/jsMain/kotlin/nl/astraeus/handler/AudioModule.kt @@ -69,7 +69,7 @@ abstract class AudioNode( node.connect(module.audioContext.destination) } else { node.connect(destination, outputIndex, inputIndex) - } + } node.port.onmessage = ::onMessage @@ -80,5 +80,4 @@ abstract class AudioNode( done(node) } } - -} \ No newline at end of file +} diff --git a/src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt b/src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt index 7e4608b..effab7f 100644 --- a/src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt +++ b/src/jsWorkletMain/kotlin/nl/astraeus/worklet/Main.kt @@ -16,7 +16,7 @@ fun main() { if (event is MessageEvent) { val data: dynamic = event.data - console.log("DATA: ", data, data?.command == "audio-processor-message-port") + console.log("WL DATA: ", data, data?.command == "audio-processor-message-port") if (data?.command == "audio-processor-message-port") { port = data.port port?.onmessage = { diff --git a/src/jvmMain/kotlin/IndexHtml.kt b/src/jvmMain/kotlin/IndexHtml.kt index a8c0e3a..c0ddfc9 100644 --- a/src/jvmMain/kotlin/IndexHtml.kt +++ b/src/jvmMain/kotlin/IndexHtml.kt @@ -2,6 +2,7 @@ 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 @@ -67,23 +68,46 @@ fun HTML.index() { id = "harmonics" type = InputType.number value = "3" - min = "1" + min = "0" max = "10" } } + h3 { + + "Node 1" + } div { input { - id = "note_c3" + id = "note_c3_1" type = InputType.button value = "C3" } input { - id = "note_e3" + id = "note_e3_1" type = InputType.button value = "E3" } input { - id = "note_g3" + 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" } diff --git a/src/jvmMain/kotlin/WorkletHtml.kt b/src/jvmMain/kotlin/WorkletHtml.kt index 32225e8..4c474fb 100644 --- a/src/jvmMain/kotlin/WorkletHtml.kt +++ b/src/jvmMain/kotlin/WorkletHtml.kt @@ -30,7 +30,7 @@ fun HTML.worklet() { id = "harmonics" type = InputType.number value = "3" - min = "1" + min = "0" max = "10" } }